@next/codemod 16.3.0-canary.8 → 16.3.0-canary.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agents-md.js +28 -12
- package/bin/next-codemod.js +1 -0
- package/bin/upgrade.js +126 -29
- package/lib/__tests__/agents-md-e2e.test.js +117 -0
- package/lib/agents-md.js +32 -0
- package/lib/handle-package.js +44 -0
- package/lib/utils.js +5 -0
- package/package.json +10 -9
- package/transforms/cache-components-instant-false.js +129 -0
- package/transforms/next-experimental-turbo-to-turbopack.js +48 -4
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
|
@@ -114,6 +114,10 @@ function resolveSemanticRevision(revision, installedVersion) {
|
|
|
114
114
|
}
|
|
115
115
|
async function runUpgrade(revision, options) {
|
|
116
116
|
const { verbose } = options;
|
|
117
|
+
const nonInteractive = options.yes === true || !process.stdin.isTTY;
|
|
118
|
+
if (nonInteractive) {
|
|
119
|
+
console.log(` Running in non-interactive mode. Every prompt will accept its default.`);
|
|
120
|
+
}
|
|
117
121
|
const appPackageJsonPath = path_1.default.resolve(cwd, 'package.json');
|
|
118
122
|
let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
|
|
119
123
|
const installedNextVersion = getInstalledNextVersion();
|
|
@@ -189,18 +193,24 @@ async function runUpgrade(revision, options) {
|
|
|
189
193
|
// The mixed case is tricky to handle from a types perspective.
|
|
190
194
|
// We'll recommend to upgrade in the prompt but users can decide to try 18.
|
|
191
195
|
!isPureAppRouter) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
if (nonInteractive) {
|
|
197
|
+
// Default: upgrade React past 18.
|
|
198
|
+
shouldStayOnReact18 = false;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const shouldStayOnReact18Res = await (0, prompts_1.default)({
|
|
202
|
+
type: 'confirm',
|
|
203
|
+
name: 'shouldStayOnReact18',
|
|
204
|
+
message: `Do you prefer to stay on React 18?` +
|
|
205
|
+
(isMixedApp
|
|
206
|
+
? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
|
|
207
|
+
: ''),
|
|
208
|
+
initial: false,
|
|
209
|
+
active: 'Yes',
|
|
210
|
+
inactive: 'No',
|
|
211
|
+
}, { onCancel: utils_1.onCancel });
|
|
212
|
+
shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
|
|
213
|
+
}
|
|
204
214
|
}
|
|
205
215
|
// We're resolving a specific version here to avoid including "ugly" version queries
|
|
206
216
|
// in the manifest.
|
|
@@ -212,9 +222,9 @@ async function runUpgrade(revision, options) {
|
|
|
212
222
|
: await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
|
|
213
223
|
if ((0, semver_1.compare)(targetNextVersion, '15.0.0-canary') >= 0 &&
|
|
214
224
|
(0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
|
|
215
|
-
await suggestTurbopack(appPackageJson, targetNextVersion);
|
|
225
|
+
await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
|
|
216
226
|
}
|
|
217
|
-
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
|
|
227
|
+
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
|
|
218
228
|
const packageManager = (0, handle_package_1.getPkgManager)(cwd);
|
|
219
229
|
let shouldRunReactCodemods = false;
|
|
220
230
|
let shouldRunReactTypesCodemods = false;
|
|
@@ -223,8 +233,9 @@ async function runUpgrade(revision, options) {
|
|
|
223
233
|
if (!shouldStayOnReact18 &&
|
|
224
234
|
(0, semver_1.compare)(targetReactVersion, '19.0.0-0') >= 0 &&
|
|
225
235
|
(0, semver_1.compare)(installedReactVersion, '19.0.0-0') < 0) {
|
|
226
|
-
shouldRunReactCodemods = await suggestReactCodemods();
|
|
227
|
-
shouldRunReactTypesCodemods =
|
|
236
|
+
shouldRunReactCodemods = await suggestReactCodemods(nonInteractive);
|
|
237
|
+
shouldRunReactTypesCodemods =
|
|
238
|
+
await suggestReactTypesCodemods(nonInteractive);
|
|
228
239
|
execCommand = getNpxCommand(packageManager);
|
|
229
240
|
}
|
|
230
241
|
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
|
|
@@ -284,6 +295,37 @@ async function runUpgrade(revision, options) {
|
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
}
|
|
298
|
+
// Bump `eslint` alongside `eslint-config-next` so the install doesn't fail
|
|
299
|
+
// on a peer-dep mismatch. e.g. `eslint-config-next@16.x` requires
|
|
300
|
+
// `eslint@>=9`, but a project upgrading from Next 15 will still have
|
|
301
|
+
// `eslint@^8` from create-next-app. Skip silently if anything goes wrong;
|
|
302
|
+
// the worst case is the user hits the same peer-dep error they would have
|
|
303
|
+
// without this bump.
|
|
304
|
+
//
|
|
305
|
+
// Only act when the project is actually using `eslint-config-next` — we
|
|
306
|
+
// don't want to silently upgrade eslint majors for projects that use
|
|
307
|
+
// eslint for unrelated reasons.
|
|
308
|
+
if (allDependencies['eslint'] && allDependencies['eslint-config-next']) {
|
|
309
|
+
try {
|
|
310
|
+
const eslintConfigNextPeerDepsJSON = (0, child_process_1.execSync)(`npm --silent view "eslint-config-next@${targetNextVersion}" peerDependencies --json`, { encoding: 'utf-8' });
|
|
311
|
+
const eslintConfigNextPeerDeps = eslintConfigNextPeerDepsJSON.trim() === ''
|
|
312
|
+
? {}
|
|
313
|
+
: JSON.parse(eslintConfigNextPeerDepsJSON);
|
|
314
|
+
const eslintRange = eslintConfigNextPeerDeps?.eslint;
|
|
315
|
+
if (eslintRange) {
|
|
316
|
+
const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${eslintRange}`);
|
|
317
|
+
versionMapping['eslint'] = {
|
|
318
|
+
version: targetEslintVersion,
|
|
319
|
+
required: false,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
if (verbose) {
|
|
325
|
+
console.warn(` Could not determine eslint peer range from eslint-config-next@${targetNextVersion}. Leaving eslint version alone.`, e);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
287
329
|
// Even though we only need those if we alias `@types/react` to types-react,
|
|
288
330
|
// we still do it out of safety due to https://github.com/microsoft/DefinitelyTyped-tools/issues/433.
|
|
289
331
|
const overrides = {};
|
|
@@ -320,10 +362,11 @@ async function runUpgrade(revision, options) {
|
|
|
320
362
|
// understanding of the codemods, we run all of the applicable codemods.
|
|
321
363
|
if (shouldRunReactCodemods) {
|
|
322
364
|
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
|
|
323
|
-
(0, child_process_1.execSync)(
|
|
324
365
|
// `--no-interactive` skips the interactive prompt that asks for confirmation
|
|
325
366
|
// https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
|
|
326
|
-
|
|
367
|
+
// `--allow-dirty` is required because the upgrade above modified package.json
|
|
368
|
+
// and the lockfile; the recipe refuses to run on a dirty tree otherwise.
|
|
369
|
+
(0, child_process_1.execSync)(`${execCommand} codemod@latest react/19/migration-recipe --no-interactive --allow-dirty`, { stdio: 'inherit' });
|
|
327
370
|
}
|
|
328
371
|
if (shouldRunReactTypesCodemods) {
|
|
329
372
|
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
|
|
@@ -383,7 +426,7 @@ function isUsingAppDir(projectPath) {
|
|
|
383
426
|
* 3. Otherwise, we ask the user to manually add `--turbopack` to their dev command,
|
|
384
427
|
* showing the current dev command as the initial value.
|
|
385
428
|
*/
|
|
386
|
-
async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
429
|
+
async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive) {
|
|
387
430
|
const devScript = packageJson.scripts?.['dev'];
|
|
388
431
|
// Turbopack flag was changed from `--turbo` to `--turbopack` in v15.0.1-canary.3
|
|
389
432
|
// PR: https://github.com/vercel/next.js/pull/71657
|
|
@@ -406,19 +449,28 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
|
406
449
|
}
|
|
407
450
|
return;
|
|
408
451
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
452
|
+
let enable = true;
|
|
453
|
+
if (!nonInteractive) {
|
|
454
|
+
const responseTurbopack = await (0, prompts_1.default)({
|
|
455
|
+
type: 'confirm',
|
|
456
|
+
name: 'enable',
|
|
457
|
+
message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
|
|
458
|
+
initial: true,
|
|
459
|
+
}, { onCancel: utils_1.onCancel });
|
|
460
|
+
enable = responseTurbopack.enable;
|
|
461
|
+
}
|
|
462
|
+
if (!enable) {
|
|
416
463
|
return;
|
|
417
464
|
}
|
|
418
465
|
packageJson.scripts['dev'] = devScript.replace('next dev', `next dev ${turboPackFlag}`);
|
|
419
466
|
return;
|
|
420
467
|
}
|
|
421
468
|
console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
|
|
469
|
+
if (nonInteractive) {
|
|
470
|
+
// Without a TTY we can't ask the user for a replacement script.
|
|
471
|
+
// Keep the existing dev script untouched.
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
422
474
|
const responseCustomDevScript = await (0, prompts_1.default)({
|
|
423
475
|
type: 'text',
|
|
424
476
|
name: 'customDevScript',
|
|
@@ -428,7 +480,7 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
|
428
480
|
packageJson.scripts['dev'] =
|
|
429
481
|
responseCustomDevScript.customDevScript || devScript;
|
|
430
482
|
}
|
|
431
|
-
async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
483
|
+
async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
|
|
432
484
|
// example:
|
|
433
485
|
// codemod version: 15.0.0-canary.45
|
|
434
486
|
// 14.3 -> 15.0.0-canary.45: apply
|
|
@@ -451,6 +503,13 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
451
503
|
if (relevantCodemods.length === 0) {
|
|
452
504
|
return [];
|
|
453
505
|
}
|
|
506
|
+
if (nonInteractive) {
|
|
507
|
+
// Default: apply every recommended codemod, matching `selected: true` below.
|
|
508
|
+
const all = relevantCodemods.map(({ value }) => value);
|
|
509
|
+
console.log(` Applying all ${picocolors_1.default.blue('codemods')} recommended for your upgrade:\n` +
|
|
510
|
+
all.map((value) => ` - ${value}`).join('\n'));
|
|
511
|
+
return all;
|
|
512
|
+
}
|
|
454
513
|
const { codemods } = await (0, prompts_1.default)({
|
|
455
514
|
type: 'multiselect',
|
|
456
515
|
name: 'codemods',
|
|
@@ -466,7 +525,10 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
466
525
|
}, { onCancel: utils_1.onCancel });
|
|
467
526
|
return codemods;
|
|
468
527
|
}
|
|
469
|
-
async function suggestReactCodemods() {
|
|
528
|
+
async function suggestReactCodemods(nonInteractive) {
|
|
529
|
+
if (nonInteractive) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
470
532
|
const { runReactCodemod } = await (0, prompts_1.default)({
|
|
471
533
|
type: 'confirm',
|
|
472
534
|
name: 'runReactCodemod',
|
|
@@ -475,7 +537,10 @@ async function suggestReactCodemods() {
|
|
|
475
537
|
}, { onCancel: utils_1.onCancel });
|
|
476
538
|
return runReactCodemod;
|
|
477
539
|
}
|
|
478
|
-
async function suggestReactTypesCodemods() {
|
|
540
|
+
async function suggestReactTypesCodemods(nonInteractive) {
|
|
541
|
+
if (nonInteractive) {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
479
544
|
const { runReactTypesCodemod } = await (0, prompts_1.default)({
|
|
480
545
|
type: 'confirm',
|
|
481
546
|
name: 'runReactTypesCodemod',
|
|
@@ -500,6 +565,16 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
500
565
|
}
|
|
501
566
|
}
|
|
502
567
|
else if (packageManager === 'pnpm') {
|
|
568
|
+
// pnpm v11 silently ignores `pnpm.overrides` in package.json. The
|
|
569
|
+
// canonical location moved to `pnpm-workspace.yaml#overrides`.
|
|
570
|
+
// See https://pnpm.io/settings and https://github.com/pnpm/pnpm/issues/11536.
|
|
571
|
+
// When the version cannot be detected, assume the current (v11+) layout
|
|
572
|
+
// since that's the surface where silently-dropped overrides hurt most.
|
|
573
|
+
const pnpmMajorVersion = (0, handle_package_1.getPnpmMajorVersion)();
|
|
574
|
+
if (pnpmMajorVersion === null || pnpmMajorVersion >= 11) {
|
|
575
|
+
writePnpmWorkspaceOverrides(overrides);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
503
578
|
// pnpm supports pnpm.overrides and pnpm.resolutions
|
|
504
579
|
if (packageJson.resolutions) {
|
|
505
580
|
for (const [key, value] of entries) {
|
|
@@ -545,6 +620,28 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
545
620
|
}
|
|
546
621
|
}
|
|
547
622
|
}
|
|
623
|
+
function writePnpmWorkspaceOverrides(overrides) {
|
|
624
|
+
// Deferred require so `js-yaml` is only loaded when we hit the pnpm v11+
|
|
625
|
+
// branch (i.e. not for npm/yarn/bun/pnpm-v10 upgrades). The package is CJS,
|
|
626
|
+
// so a sync `require()` keeps this function synchronous.
|
|
627
|
+
const yaml = require('js-yaml');
|
|
628
|
+
const filePath = path_1.default.join(cwd, 'pnpm-workspace.yaml');
|
|
629
|
+
let doc = {};
|
|
630
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
631
|
+
const existing = fs_1.default.readFileSync(filePath, 'utf8');
|
|
632
|
+
const parsed = yaml.load(existing);
|
|
633
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
634
|
+
doc = parsed;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!doc.overrides || typeof doc.overrides !== 'object') {
|
|
638
|
+
doc.overrides = {};
|
|
639
|
+
}
|
|
640
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
641
|
+
doc.overrides[key] = value;
|
|
642
|
+
}
|
|
643
|
+
fs_1.default.writeFileSync(filePath, yaml.dump(doc));
|
|
644
|
+
}
|
|
548
645
|
function warnDependenciesOutOfRange(appPackageJson, versionMapping) {
|
|
549
646
|
const allDirectDependencies = {
|
|
550
647
|
...appPackageJson.dependencies,
|
|
@@ -293,6 +293,123 @@ This is my project documentation.
|
|
|
293
293
|
}
|
|
294
294
|
}, 30000) // Increase timeout for git clone
|
|
295
295
|
|
|
296
|
+
describe('bundled docs (Next.js >= 16.2.0)', () => {
|
|
297
|
+
// Simulate an install of a Next.js version that ships docs inside the
|
|
298
|
+
// published package at node_modules/next/dist/docs.
|
|
299
|
+
function setupBundledNext(projectDir, version) {
|
|
300
|
+
const nextDir = path.join(projectDir, 'node_modules', 'next')
|
|
301
|
+
const gettingStartedDir = path.join(
|
|
302
|
+
nextDir,
|
|
303
|
+
'dist',
|
|
304
|
+
'docs',
|
|
305
|
+
'01-app',
|
|
306
|
+
'01-getting-started'
|
|
307
|
+
)
|
|
308
|
+
fs.mkdirSync(gettingStartedDir, { recursive: true })
|
|
309
|
+
fs.writeFileSync(
|
|
310
|
+
path.join(nextDir, 'package.json'),
|
|
311
|
+
JSON.stringify({ name: 'next', version })
|
|
312
|
+
)
|
|
313
|
+
fs.writeFileSync(
|
|
314
|
+
path.join(nextDir, 'dist', 'docs', 'index.md'),
|
|
315
|
+
'# Next.js Docs'
|
|
316
|
+
)
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(gettingStartedDir, '01-installation.md'),
|
|
319
|
+
'# Installation'
|
|
320
|
+
)
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(gettingStartedDir, '02-project-structure.md'),
|
|
323
|
+
'# Project Structure'
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
it('indexes bundled docs instead of downloading when installed Next.js ships them', async () => {
|
|
328
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
329
|
+
|
|
330
|
+
const originalCwd = process.cwd()
|
|
331
|
+
process.chdir(testProjectDir)
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await runAgentsMd({ output: 'CLAUDE.md' })
|
|
335
|
+
|
|
336
|
+
// No .next-docs copy and no .gitignore entry for it
|
|
337
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
338
|
+
false
|
|
339
|
+
)
|
|
340
|
+
expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
|
|
341
|
+
false
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
const claudeMdContent = fs.readFileSync(
|
|
345
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
346
|
+
'utf-8'
|
|
347
|
+
)
|
|
348
|
+
expect(claudeMdContent).toContain(
|
|
349
|
+
'root: ./node_modules/next/dist/docs'
|
|
350
|
+
)
|
|
351
|
+
expect(claudeMdContent).toContain('01-installation.md')
|
|
352
|
+
|
|
353
|
+
const output = consoleOutput.join('\n')
|
|
354
|
+
expect(output).toContain('bundled with Next.js')
|
|
355
|
+
expect(output).not.toContain('Downloading')
|
|
356
|
+
} finally {
|
|
357
|
+
process.chdir(originalCwd)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('uses bundled docs when --version matches the installed version', async () => {
|
|
362
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
363
|
+
|
|
364
|
+
const originalCwd = process.cwd()
|
|
365
|
+
process.chdir(testProjectDir)
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await runAgentsMd({ version: '16.2.0', output: 'AGENTS.md' })
|
|
369
|
+
|
|
370
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
371
|
+
false
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const agentsMdContent = fs.readFileSync(
|
|
375
|
+
path.join(testProjectDir, 'AGENTS.md'),
|
|
376
|
+
'utf-8'
|
|
377
|
+
)
|
|
378
|
+
expect(agentsMdContent).toContain(
|
|
379
|
+
'root: ./node_modules/next/dist/docs'
|
|
380
|
+
)
|
|
381
|
+
} finally {
|
|
382
|
+
process.chdir(originalCwd)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('falls back to downloading when --version differs from the installed version', async () => {
|
|
387
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
388
|
+
|
|
389
|
+
const originalCwd = process.cwd()
|
|
390
|
+
process.chdir(testProjectDir)
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await runAgentsMd({ version: '15.0.0', output: 'CLAUDE.md' })
|
|
394
|
+
|
|
395
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
396
|
+
true
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
const claudeMdContent = fs.readFileSync(
|
|
400
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
401
|
+
'utf-8'
|
|
402
|
+
)
|
|
403
|
+
expect(claudeMdContent).toContain('root: ./.next-docs')
|
|
404
|
+
|
|
405
|
+
const output = consoleOutput.join('\n')
|
|
406
|
+
expect(output).toContain('Downloading')
|
|
407
|
+
} finally {
|
|
408
|
+
process.chdir(originalCwd)
|
|
409
|
+
}
|
|
410
|
+
}, 30000) // Increase timeout for git clone
|
|
411
|
+
})
|
|
412
|
+
|
|
296
413
|
describe('getNextjsVersion', () => {
|
|
297
414
|
const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
|
|
298
415
|
|
package/lib/agents-md.js
CHANGED
|
@@ -10,6 +10,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.getNextjsVersion = getNextjsVersion;
|
|
13
|
+
exports.getBundledDocsInfo = getBundledDocsInfo;
|
|
14
|
+
exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
|
|
13
15
|
exports.pullDocs = pullDocs;
|
|
14
16
|
exports.collectDocFiles = collectDocFiles;
|
|
15
17
|
exports.buildDocTree = buildDocTree;
|
|
@@ -45,6 +47,36 @@ function getNextjsVersion(cwd) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Next.js ships its documentation inside the published package (at
|
|
52
|
+
* `dist/docs`) since 16.2.0. When the install resolved from `cwd` has
|
|
53
|
+
* bundled docs, the index can point at them directly instead of
|
|
54
|
+
* downloading a copy into `.next-docs`.
|
|
55
|
+
*/
|
|
56
|
+
function getBundledDocsInfo(cwd) {
|
|
57
|
+
try {
|
|
58
|
+
const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
|
|
59
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
|
|
60
|
+
const docsPath = path_1.default.join(path_1.default.dirname(nextPkgPath), 'dist', 'docs');
|
|
61
|
+
if (!pkg.version || collectDocFiles(docsPath).length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return { docsPath, version: pkg.version };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function getBundledDocsLinkPath(cwd, docsPath) {
|
|
71
|
+
// Prefer the conventional path when it resolves from the project
|
|
72
|
+
// (covers hoisted installs; pnpm exposes next via a node_modules symlink).
|
|
73
|
+
const conventional = path_1.default.join(cwd, 'node_modules', 'next', 'dist', 'docs');
|
|
74
|
+
if (fs_1.default.existsSync(conventional)) {
|
|
75
|
+
return './node_modules/next/dist/docs';
|
|
76
|
+
}
|
|
77
|
+
const relative = path_1.default.relative(cwd, docsPath).replace(/\\/g, '/');
|
|
78
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
79
|
+
}
|
|
48
80
|
function versionToGitHubTag(version) {
|
|
49
81
|
return version.startsWith('v') ? version : `v${version}`;
|
|
50
82
|
}
|
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,7 +12,49 @@ exports.runInstallation = runInstallation;
|
|
|
10
12
|
exports.addPackageDependency = addPackageDependency;
|
|
11
13
|
const find_up_1 = __importDefault(require("find-up"));
|
|
12
14
|
const execa_1 = __importDefault(require("execa"));
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
16
|
const node_path_1 = require("node:path");
|
|
17
|
+
/**
|
|
18
|
+
* Get the full version string for the given package manager.
|
|
19
|
+
*
|
|
20
|
+
* First tries to parse from `npm_config_user_agent` (e.g., "pnpm/9.13.2 npm/? ..."),
|
|
21
|
+
* then falls back to spawning `<packageManager> --version`.
|
|
22
|
+
*
|
|
23
|
+
* Returns null if unable to determine the version.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `packages/create-next-app/helpers/get-pkg-manager.ts`.
|
|
26
|
+
*/
|
|
27
|
+
function getPackageManagerVersion(packageManager) {
|
|
28
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
29
|
+
const userAgentMatch = userAgent.match(new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`));
|
|
30
|
+
if (userAgentMatch) {
|
|
31
|
+
return userAgentMatch[1];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const version = (0, node_child_process_1.execSync)(`${packageManager} --version`, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
37
|
+
}).trim();
|
|
38
|
+
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
39
|
+
return version;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// package manager not available or failed to run
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the major version of pnpm being used.
|
|
49
|
+
* Returns null if unable to determine the version.
|
|
50
|
+
*/
|
|
51
|
+
function getPnpmMajorVersion() {
|
|
52
|
+
const version = getPackageManagerVersion('pnpm');
|
|
53
|
+
if (!version)
|
|
54
|
+
return null;
|
|
55
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
56
|
+
return Number.isNaN(major) ? null : major;
|
|
57
|
+
}
|
|
14
58
|
function getPkgManager(baseDir) {
|
|
15
59
|
try {
|
|
16
60
|
const lockFile = find_up_1.default.sync([
|
package/lib/utils.js
CHANGED
|
@@ -132,5 +132,10 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
132
132
|
value: 'remove-experimental-ppr',
|
|
133
133
|
version: '16.0.0-canary.11',
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
|
|
137
|
+
value: 'cache-components-instant-false',
|
|
138
|
+
version: '16.3.0',
|
|
139
|
+
},
|
|
135
140
|
];
|
|
136
141
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next/codemod",
|
|
3
|
-
"version": "16.3.0-canary.
|
|
3
|
+
"version": "16.3.0-canary.80",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"find-up": "4.1.0",
|
|
15
15
|
"globby": "11.0.1",
|
|
16
16
|
"is-git-clean": "1.1.0",
|
|
17
|
+
"js-yaml": "4.2.0",
|
|
17
18
|
"jscodeshift": "17.0.0",
|
|
18
19
|
"picocolors": "1.0.0",
|
|
19
20
|
"prompts": "2.4.2",
|
|
@@ -27,19 +28,19 @@
|
|
|
27
28
|
"lib/**/*.js",
|
|
28
29
|
"lib/cra-to-next/gitignore"
|
|
29
30
|
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "pnpm tsc -d -p tsconfig.json",
|
|
32
|
-
"prepublishOnly": "cd ../../ && turbo run build",
|
|
33
|
-
"dev": "pnpm tsc -d -w -p tsconfig.json",
|
|
34
|
-
"test": "jest",
|
|
35
|
-
"test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
|
|
36
|
-
},
|
|
37
31
|
"bin": "./bin/next-codemod.js",
|
|
38
32
|
"devDependencies": {
|
|
39
33
|
"@types/find-up": "4.0.0",
|
|
34
|
+
"@types/js-yaml": "4.0.9",
|
|
40
35
|
"@types/jscodeshift": "0.11.0",
|
|
41
36
|
"@types/prompts": "2.4.2",
|
|
42
37
|
"@types/semver": "7.3.1",
|
|
43
38
|
"typescript": "6.0.2"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "pnpm tsc -d -p tsconfig.json",
|
|
42
|
+
"dev": "pnpm tsc -d -w -p tsconfig.json",
|
|
43
|
+
"test": "jest",
|
|
44
|
+
"test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = transformer;
|
|
4
|
+
const parser_1 = require("../lib/parser");
|
|
5
|
+
/**
|
|
6
|
+
* Blanket-inserts `export const instant = false` into every App Router `page`,
|
|
7
|
+
* `layout`, and `default` file so they're marked as allowed to block when
|
|
8
|
+
* `cacheComponents` is enabled. Each opt-out is meant to be walked back, one
|
|
9
|
+
* route at a time, using the companion adoption skill.
|
|
10
|
+
*
|
|
11
|
+
* - Skips files that already declare or export `instant` in any form (never
|
|
12
|
+
* overrides existing config or appends a duplicate binding).
|
|
13
|
+
* - Skips Client/Server Component modules (`"use client"` / `"use server"`):
|
|
14
|
+
* `instant` is a Server Component route segment config, so exporting it from
|
|
15
|
+
* those modules is a build error.
|
|
16
|
+
* - Targets `page` / `layout` / `default` only (not `route` — `instant` does
|
|
17
|
+
* not apply to route handlers). `default.tsx` is the parallel-route fallback,
|
|
18
|
+
* a server segment that accepts route segment config like the other two.
|
|
19
|
+
*/
|
|
20
|
+
function transformer(file, _api) {
|
|
21
|
+
if (process.env.NODE_ENV !== 'test' &&
|
|
22
|
+
!/(^|[/\\])app[/\\].*?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
|
|
23
|
+
return file.source;
|
|
24
|
+
}
|
|
25
|
+
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
26
|
+
const root = j(file.source);
|
|
27
|
+
// Bail on Client/Server Component modules. `instant` is a Server Component
|
|
28
|
+
// route segment config; exporting it from a `"use client"` (or `"use server"`)
|
|
29
|
+
// module fails the build. Parsers represent the directive either in
|
|
30
|
+
// `program.directives` or as a leading string-literal `ExpressionStatement`.
|
|
31
|
+
const program = root.get().node.program;
|
|
32
|
+
const isClientOrServerDirective = (value) => value === 'use client' || value === 'use server';
|
|
33
|
+
let hasModuleDirective = (program.directives ?? []).some((d) => isClientOrServerDirective(d?.value?.value));
|
|
34
|
+
if (!hasModuleDirective) {
|
|
35
|
+
for (const node of program.body) {
|
|
36
|
+
if (node.type !== 'ExpressionStatement' ||
|
|
37
|
+
(node.expression?.type !== 'StringLiteral' &&
|
|
38
|
+
node.expression?.type !== 'Literal')) {
|
|
39
|
+
// Directives must lead the module; stop at the first non-directive.
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (isClientOrServerDirective(node.expression.value)) {
|
|
43
|
+
hasModuleDirective = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (hasModuleDirective) {
|
|
49
|
+
return file.source;
|
|
50
|
+
}
|
|
51
|
+
// Bail if `instant` already exists in any form, so we never append a
|
|
52
|
+
// duplicate declaration (which would be a `SyntaxError`). This covers:
|
|
53
|
+
// export const instant = ...
|
|
54
|
+
// export const a = 1, instant = ... (any declarator position)
|
|
55
|
+
// const instant = ... (local binding)
|
|
56
|
+
// const { instant } = ... (destructured binding)
|
|
57
|
+
// export { instant }
|
|
58
|
+
// export { foo as instant }
|
|
59
|
+
// export function instant() {} / export class instant {}
|
|
60
|
+
const bindsInstant = (node) => {
|
|
61
|
+
switch (node?.type) {
|
|
62
|
+
case 'Identifier':
|
|
63
|
+
return node.name === 'instant';
|
|
64
|
+
case 'ObjectPattern':
|
|
65
|
+
return node.properties.some((prop) => prop.type === 'RestElement'
|
|
66
|
+
? bindsInstant(prop.argument)
|
|
67
|
+
: bindsInstant(prop.value ?? prop.argument));
|
|
68
|
+
case 'ArrayPattern':
|
|
69
|
+
return node.elements.some((el) => el != null && bindsInstant(el));
|
|
70
|
+
case 'AssignmentPattern':
|
|
71
|
+
return bindsInstant(node.left);
|
|
72
|
+
case 'RestElement':
|
|
73
|
+
return bindsInstant(node.argument);
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const hasInstantBinding = root
|
|
79
|
+
.find(j.VariableDeclarator)
|
|
80
|
+
.filter((p) => bindsInstant(p.node.id))
|
|
81
|
+
.size() > 0 ||
|
|
82
|
+
root.find(j.ExportSpecifier, { exported: { name: 'instant' } }).size() >
|
|
83
|
+
0 ||
|
|
84
|
+
root.find(j.FunctionDeclaration, { id: { name: 'instant' } }).size() > 0 ||
|
|
85
|
+
root.find(j.ClassDeclaration, { id: { name: 'instant' } }).size() > 0;
|
|
86
|
+
if (hasInstantBinding) {
|
|
87
|
+
return file.source;
|
|
88
|
+
}
|
|
89
|
+
// Build `export const instant = false`. The two `//` comments above it
|
|
90
|
+
// (TODO + See:) are attached as leading comments on the declaration so
|
|
91
|
+
// recast prints them right above it.
|
|
92
|
+
const todoComment = j.commentLine(' TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.', true, false);
|
|
93
|
+
const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
|
|
94
|
+
const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
|
|
95
|
+
j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
|
|
96
|
+
]));
|
|
97
|
+
instantExport.comments = [todoComment, seeComment];
|
|
98
|
+
// Insert after the last top-level import, or at the top of the module
|
|
99
|
+
// if there are no imports.
|
|
100
|
+
const body = program.body;
|
|
101
|
+
let lastImportIndex = -1;
|
|
102
|
+
for (let i = 0; i < body.length; i++) {
|
|
103
|
+
if (body[i].type === 'ImportDeclaration')
|
|
104
|
+
lastImportIndex = i;
|
|
105
|
+
}
|
|
106
|
+
if (lastImportIndex !== -1) {
|
|
107
|
+
body.splice(lastImportIndex + 1, 0, instantExport);
|
|
108
|
+
}
|
|
109
|
+
else if (body.length > 0) {
|
|
110
|
+
// No imports. Inserting at index 0 would steal any file-level leading
|
|
111
|
+
// comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
|
|
112
|
+
// attributes them to whatever is first. Move those leading comments
|
|
113
|
+
// off `body[0]` onto the new export *before* its TODO/See: lines, so
|
|
114
|
+
// they print in their original position.
|
|
115
|
+
const first = body[0];
|
|
116
|
+
const allComments = (first.comments ?? []);
|
|
117
|
+
const firstLeading = allComments.filter((c) => c.leading === true);
|
|
118
|
+
if (firstLeading.length > 0) {
|
|
119
|
+
first.comments = allComments.filter((c) => c.leading !== true);
|
|
120
|
+
instantExport.comments = [...firstLeading, todoComment, seeComment];
|
|
121
|
+
}
|
|
122
|
+
body.unshift(instantExport);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
body.push(instantExport);
|
|
126
|
+
}
|
|
127
|
+
return root.toSource();
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=cache-components-instant-false.js.map
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* the new top-level `turbopack` configuration.
|
|
5
5
|
*
|
|
6
6
|
* It moves most properties from experimental.turbo to the top-level turbopack
|
|
7
|
-
* property, with special handling for certain properties like
|
|
7
|
+
* property, with special handling for certain properties like minify,
|
|
8
8
|
* treeShaking, and sourceMaps which become experimental.turbopack* properties instead.
|
|
9
|
+
*
|
|
10
|
+
* The `memoryLimit` option is no longer supported, so it is removed entirely in
|
|
11
|
+
* both its old location (`experimental.turbo.memoryLimit`) and the location the
|
|
12
|
+
* previous version of this codemod produced (`experimental.turbopackMemoryLimit`).
|
|
9
13
|
*/
|
|
10
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
15
|
exports.default = transformer;
|
|
@@ -13,11 +17,15 @@ const parser_1 = require("../lib/parser");
|
|
|
13
17
|
const utils_1 = require("./lib/utils");
|
|
14
18
|
// Properties that need to be moved to experimental.turbopack*
|
|
15
19
|
const RENAMED_EXPERIMENTAL_PROPERTIES = {
|
|
16
|
-
memoryLimit: 'turbopackMemoryLimit',
|
|
17
20
|
minify: 'turbopackMinify',
|
|
18
21
|
treeShaking: 'turbopackTreeShaking',
|
|
19
22
|
sourceMaps: 'turbopackSourceMaps',
|
|
20
23
|
};
|
|
24
|
+
// `memoryLimit` is no longer supported and is removed entirely. We drop it under
|
|
25
|
+
// both its old `experimental.turbo.*` name and the `experimental.*` name produced
|
|
26
|
+
// by the previous version of this codemod.
|
|
27
|
+
const REMOVED_TURBO_PROPERTY = 'memoryLimit';
|
|
28
|
+
const REMOVED_EXPERIMENTAL_PROPERTY = 'turbopackMemoryLimit';
|
|
21
29
|
function transformer(file, _api, options) {
|
|
22
30
|
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
23
31
|
const root = j(file.source);
|
|
@@ -56,6 +64,13 @@ function transformer(file, _api, options) {
|
|
|
56
64
|
const regularProps = [];
|
|
57
65
|
const specialProps = [];
|
|
58
66
|
turboObj.properties.forEach((prop) => {
|
|
67
|
+
if (isStaticProperty(prop) &&
|
|
68
|
+
prop.key &&
|
|
69
|
+
prop.key.type === 'Identifier' &&
|
|
70
|
+
prop.key.name === REMOVED_TURBO_PROPERTY) {
|
|
71
|
+
// Drop: `memoryLimit` is no longer supported.
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
59
74
|
if (isStaticProperty(prop) &&
|
|
60
75
|
prop.key &&
|
|
61
76
|
prop.key.type === 'Identifier' &&
|
|
@@ -71,7 +86,10 @@ function transformer(file, _api, options) {
|
|
|
71
86
|
const existingProps = experimentalObj.properties.filter((prop) => !(isStaticProperty(prop) &&
|
|
72
87
|
prop.key &&
|
|
73
88
|
prop.key.type === 'Identifier' &&
|
|
74
|
-
|
|
89
|
+
// Drop the `turbo` object (moved above) and a pre-existing
|
|
90
|
+
// `turbopackMemoryLimit` (no longer supported).
|
|
91
|
+
(prop.key.name === 'turbo' ||
|
|
92
|
+
prop.key.name === REMOVED_EXPERIMENTAL_PROPERTY)));
|
|
75
93
|
experimentalObj.properties = [...existingProps, ...specialProps];
|
|
76
94
|
// If experimental has no properties, remove it
|
|
77
95
|
if (experimentalObj.properties.length === 0) {
|
|
@@ -132,7 +150,13 @@ function transformer(file, _api, options) {
|
|
|
132
150
|
else {
|
|
133
151
|
return;
|
|
134
152
|
}
|
|
135
|
-
//
|
|
153
|
+
// `memoryLimit` is no longer supported: remove the assignment entirely.
|
|
154
|
+
if (propName === REMOVED_TURBO_PROPERTY) {
|
|
155
|
+
j(path).remove();
|
|
156
|
+
hasChanges = true;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// For special properties like minify, treeShaking, etc.
|
|
136
160
|
if (propName && RENAMED_EXPERIMENTAL_PROPERTIES[propName]) {
|
|
137
161
|
const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('experimental')), j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[propName])), path.node.right);
|
|
138
162
|
j(path).replaceWith(newAssignment);
|
|
@@ -195,6 +219,26 @@ function transformer(file, _api, options) {
|
|
|
195
219
|
hasChanges = true;
|
|
196
220
|
}
|
|
197
221
|
});
|
|
222
|
+
// Remove `config.experimental.turbopackMemoryLimit = value` assignments left
|
|
223
|
+
// behind by the previous version of this codemod (no longer supported).
|
|
224
|
+
root
|
|
225
|
+
.find(j.AssignmentExpression, {
|
|
226
|
+
left: {
|
|
227
|
+
type: 'MemberExpression',
|
|
228
|
+
object: {
|
|
229
|
+
type: 'MemberExpression',
|
|
230
|
+
property: { type: 'Identifier', name: 'experimental' },
|
|
231
|
+
},
|
|
232
|
+
property: {
|
|
233
|
+
type: 'Identifier',
|
|
234
|
+
name: REMOVED_EXPERIMENTAL_PROPERTY,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
.forEach((path) => {
|
|
239
|
+
j(path).remove();
|
|
240
|
+
hasChanges = true;
|
|
241
|
+
});
|
|
198
242
|
// Only return a string if we changed the AST, otherwise return the original source
|
|
199
243
|
return hasChanges ? root.toSource(options) : file.source;
|
|
200
244
|
}
|