@next/codemod 16.4.0-canary.3 → 16.4.0-canary.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/bin/next-codemod.js +1 -0
- package/bin/transform.js +2 -2
- package/bin/upgrade.js +8 -4
- package/lib/utils.js +5 -1
- package/package.json +1 -1
- package/transforms/cache-components-instant-false.js +13 -6
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +16 -3
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +6 -5
- package/transforms/lib/async-request-api/utils.js +4 -5
- package/transforms/new-link.js +1 -1
package/README.md
CHANGED
|
@@ -7,3 +7,14 @@ Codemods are transformations that run on your codebase programmatically. This al
|
|
|
7
7
|
## Documentation
|
|
8
8
|
|
|
9
9
|
Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/app/guides/upgrading/codemods) to view the documentation for this package.
|
|
10
|
+
|
|
11
|
+
## Skip optional feature adoption
|
|
12
|
+
|
|
13
|
+
`upgrade --skip-adoption` skips codemods marked as feature adoption in the
|
|
14
|
+
registry while keeping version migrations and normal dependency selection.
|
|
15
|
+
Currently this excludes `cache-components-instant-false` and the
|
|
16
|
+
`remove-partial-prefetch` cleanup used after adopting partial prefetching.
|
|
17
|
+
Both transforms can still be run explicitly.
|
|
18
|
+
|
|
19
|
+
Combine it with `--yes` for an unattended version upgrade. Without
|
|
20
|
+
`--skip-adoption`, the existing upgrade selections are unchanged.
|
package/bin/next-codemod.js
CHANGED
|
@@ -44,6 +44,7 @@ program
|
|
|
44
44
|
.usage('[revision] [options]')
|
|
45
45
|
.option('--verbose', 'Verbose output', false)
|
|
46
46
|
.option('-y, --yes', 'Skip every interactive prompt and accept its default. Also auto-enabled when stdin is not a TTY (e.g. running under an agent or in CI).', false)
|
|
47
|
+
.option('--skip-adoption', 'Skip optional feature-adoption codemods while applying version migrations.', false)
|
|
47
48
|
.action(async (revision, options) => {
|
|
48
49
|
try {
|
|
49
50
|
await (0, upgrade_1.runUpgrade)(revision, options);
|
package/bin/transform.js
CHANGED
|
@@ -56,7 +56,7 @@ async function runTransform(transform, path, options) {
|
|
|
56
56
|
}, { onCancel: utils_1.onCancel });
|
|
57
57
|
transformer = res.transformer;
|
|
58
58
|
}
|
|
59
|
-
if (transformer === 'next-request-geo-ip') {
|
|
59
|
+
if (transformer === 'next-request-geo-ip' && !options.nonInteractive) {
|
|
60
60
|
const { isAppDeployedToVercel } = await (0, prompts_1.default)({
|
|
61
61
|
type: 'confirm',
|
|
62
62
|
name: 'isAppDeployedToVercel',
|
|
@@ -99,7 +99,7 @@ async function runTransform(transform, path, options) {
|
|
|
99
99
|
args.push('--parser=tsx');
|
|
100
100
|
args.push('--ignore-pattern=**/node_modules/**');
|
|
101
101
|
args.push('--ignore-pattern=**/.next/**');
|
|
102
|
-
args.push('--extensions=tsx,ts,jsx,js');
|
|
102
|
+
args.push('--extensions=tsx,ts,jsx,js,mjs');
|
|
103
103
|
args = args.concat(['--transform', transformerPath]);
|
|
104
104
|
if (jscodeshift) {
|
|
105
105
|
args = args.concat(jscodeshift);
|
package/bin/upgrade.js
CHANGED
|
@@ -225,7 +225,7 @@ async function runUpgrade(revision, options) {
|
|
|
225
225
|
(0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
|
|
226
226
|
await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
|
|
227
227
|
}
|
|
228
|
-
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
|
|
228
|
+
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive, options.skipAdoption);
|
|
229
229
|
const packageManager = (0, handle_package_1.getPkgManager)(cwd);
|
|
230
230
|
let shouldRunReactCodemods = false;
|
|
231
231
|
let shouldRunReactTypesCodemods = false;
|
|
@@ -357,7 +357,11 @@ async function runUpgrade(revision, options) {
|
|
|
357
357
|
os.EOL);
|
|
358
358
|
(0, handle_package_1.runInstallation)(packageManager, { cwd });
|
|
359
359
|
for (const codemod of codemods) {
|
|
360
|
-
await (0, transform_1.runTransform)(codemod, cwd, {
|
|
360
|
+
await (0, transform_1.runTransform)(codemod, cwd, {
|
|
361
|
+
force: true,
|
|
362
|
+
verbose,
|
|
363
|
+
nonInteractive,
|
|
364
|
+
});
|
|
361
365
|
}
|
|
362
366
|
// To reduce user-side burden of selecting which codemods to run as it needs additional
|
|
363
367
|
// understanding of the codemods, we run all of the applicable codemods.
|
|
@@ -489,7 +493,7 @@ async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive)
|
|
|
489
493
|
packageJson.scripts['dev'] =
|
|
490
494
|
responseCustomDevScript.customDevScript || devScript;
|
|
491
495
|
}
|
|
492
|
-
async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
|
|
496
|
+
async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive, skipAdoption = false) {
|
|
493
497
|
// example:
|
|
494
498
|
// codemod version: 15.0.0-canary.45
|
|
495
499
|
// 14.3 -> 15.0.0-canary.45: apply
|
|
@@ -508,7 +512,7 @@ async function suggestCodemods(initialNextVersion, targetNextVersion, nonInterac
|
|
|
508
512
|
if (targetVersionIndex === -1) {
|
|
509
513
|
targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.length;
|
|
510
514
|
}
|
|
511
|
-
const relevantCodemods = utils_1.TRANSFORMER_INQUIRER_CHOICES.slice(initialVersionIndex, targetVersionIndex);
|
|
515
|
+
const relevantCodemods = utils_1.TRANSFORMER_INQUIRER_CHOICES.slice(initialVersionIndex, targetVersionIndex).filter((codemod) => !skipAdoption || !codemod.adoption);
|
|
512
516
|
if (relevantCodemods.length === 0) {
|
|
513
517
|
return [];
|
|
514
518
|
}
|
package/lib/utils.js
CHANGED
|
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.TRANSFORMER_INQUIRER_CHOICES = void 0;
|
|
6
|
+
exports.TRANSFORMER_INQUIRER_CHOICES = exports.NEXT_CODEMOD_IGNORE_ERROR_PREFIX = exports.NEXT_CODEMOD_ERROR_PREFIX = void 0;
|
|
7
7
|
exports.checkGitStatus = checkGitStatus;
|
|
8
8
|
exports.onCancel = onCancel;
|
|
9
9
|
const picocolors_1 = require("picocolors");
|
|
10
10
|
const is_git_clean_1 = __importDefault(require("is-git-clean"));
|
|
11
|
+
exports.NEXT_CODEMOD_ERROR_PREFIX = '@next-codemod-error';
|
|
12
|
+
exports.NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore';
|
|
11
13
|
function checkGitStatus(force) {
|
|
12
14
|
let clean = false;
|
|
13
15
|
let errorMessage = 'Unable to determine if git directory is clean';
|
|
@@ -136,11 +138,13 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
136
138
|
title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
|
|
137
139
|
value: 'cache-components-instant-false',
|
|
138
140
|
version: '16.3.0',
|
|
141
|
+
adoption: true,
|
|
139
142
|
},
|
|
140
143
|
{
|
|
141
144
|
title: "Remove `export const prefetch = 'partial'` Route Segment Config from App Router pages and layouts after enabling `partialPrefetching` globally",
|
|
142
145
|
value: 'remove-partial-prefetch',
|
|
143
146
|
version: '16.3.0',
|
|
147
|
+
adoption: true,
|
|
144
148
|
},
|
|
145
149
|
];
|
|
146
150
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = transformer;
|
|
4
4
|
const parser_1 = require("../lib/parser");
|
|
5
|
+
const utils_1 = require("../lib/utils");
|
|
5
6
|
/**
|
|
6
7
|
* Blanket-inserts `export const instant = false` into every App Router `page`,
|
|
7
8
|
* `layout`, and `default` file so they're marked as allowed to block when
|
|
@@ -86,15 +87,16 @@ function transformer(file, _api) {
|
|
|
86
87
|
if (hasInstantBinding) {
|
|
87
88
|
return file.source;
|
|
88
89
|
}
|
|
89
|
-
// Build `export const instant = false`. The
|
|
90
|
-
//
|
|
90
|
+
// Build `export const instant = false`. The ignore reason, removal condition,
|
|
91
|
+
// and guide link are attached as leading comments on the declaration so
|
|
91
92
|
// recast prints them right above it.
|
|
92
|
-
const
|
|
93
|
+
const ignoreComment = j.commentLine(` ${utils_1.NEXT_CODEMOD_IGNORE_ERROR_PREFIX} Cache Components adoption: this segment temporarily allows blocking.`, true, false);
|
|
94
|
+
const removalComment = j.commentLine(' Remove this opt-out after verifying the segment passes validation without it.', true, false);
|
|
93
95
|
const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
|
|
94
96
|
const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
|
|
95
97
|
j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
|
|
96
98
|
]));
|
|
97
|
-
instantExport.comments = [
|
|
99
|
+
instantExport.comments = [ignoreComment, removalComment, seeComment];
|
|
98
100
|
// Insert after the last top-level import, or at the top of the module
|
|
99
101
|
// if there are no imports.
|
|
100
102
|
const body = program.body;
|
|
@@ -110,14 +112,19 @@ function transformer(file, _api) {
|
|
|
110
112
|
// No imports. Inserting at index 0 would steal any file-level leading
|
|
111
113
|
// comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
|
|
112
114
|
// attributes them to whatever is first. Move those leading comments
|
|
113
|
-
// off `body[0]` onto the new export *before* its
|
|
115
|
+
// off `body[0]` onto the new export *before* its adoption comments, so
|
|
114
116
|
// they print in their original position.
|
|
115
117
|
const first = body[0];
|
|
116
118
|
const allComments = (first.comments ?? []);
|
|
117
119
|
const firstLeading = allComments.filter((c) => c.leading === true);
|
|
118
120
|
if (firstLeading.length > 0) {
|
|
119
121
|
first.comments = allComments.filter((c) => c.leading !== true);
|
|
120
|
-
instantExport.comments = [
|
|
122
|
+
instantExport.comments = [
|
|
123
|
+
...firstLeading,
|
|
124
|
+
ignoreComment,
|
|
125
|
+
removalComment,
|
|
126
|
+
seeComment,
|
|
127
|
+
];
|
|
121
128
|
}
|
|
122
129
|
body.unshift(instantExport);
|
|
123
130
|
}
|
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.transformDynamicAPI = transformDynamicAPI;
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
const parser_1 = require("../../../lib/parser");
|
|
6
|
-
const
|
|
6
|
+
const utils_2 = require("../../../lib/utils");
|
|
7
|
+
const DYNAMIC_IMPORT_WARN_COMMENT = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} The APIs under 'next/headers' are async now, need to be manually awaited. `;
|
|
7
8
|
function findDynamicImportsAndComment(root, j) {
|
|
8
9
|
let modified = false;
|
|
9
10
|
// find all the dynamic imports of `next/headers`,
|
|
@@ -146,12 +147,12 @@ function transformDynamicAPI(source, _api, filePath) {
|
|
|
146
147
|
needsReactUseImport = true;
|
|
147
148
|
}
|
|
148
149
|
else {
|
|
149
|
-
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${
|
|
150
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} Manually await this call and refactor the function to be async `);
|
|
150
151
|
modified ||= casted;
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
else {
|
|
154
|
-
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${
|
|
155
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} please manually await this call, codemod cannot transform due to undetermined async scope `);
|
|
155
156
|
modified ||= casted;
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -227,6 +228,18 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
|
|
|
227
228
|
e.g. `<path>` is cookies(), convert it to `(<path> as unknown as UnsafeUnwrappedCookies)`
|
|
228
229
|
*/
|
|
229
230
|
const targetType = API_CAST_TYPE_MAP[originRequestApiName];
|
|
231
|
+
const repairComment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} Await this API and update its callers; remove the temporary ${targetType} cast after repairing the migration. `;
|
|
232
|
+
const parentCast = path.parentPath?.node;
|
|
233
|
+
const outerCast = path.parentPath?.parentPath?.node;
|
|
234
|
+
if (j.TSAsExpression.check(parentCast) &&
|
|
235
|
+
j.TSAsExpression.check(outerCast) &&
|
|
236
|
+
j.TSTypeReference.check(outerCast.typeAnnotation) &&
|
|
237
|
+
j.Identifier.check(outerCast.typeAnnotation.typeName) &&
|
|
238
|
+
outerCast.typeAnnotation.typeName.name === targetType) {
|
|
239
|
+
// Re-parsing attaches the leading marker to the outer cast.
|
|
240
|
+
return (0, utils_1.insertCommentOnce)(outerCast, j, repairComment);
|
|
241
|
+
}
|
|
242
|
+
(0, utils_1.insertCommentOnce)(path.node, j, repairComment);
|
|
230
243
|
const newCastExpression = j.tsAsExpression(j.tsAsExpression(path.node, j.tsUnknownKeyword()), j.tsTypeReference(j.identifier(targetType)));
|
|
231
244
|
// Replace the original expression with the new cast expression,
|
|
232
245
|
// also wrap () around the new cast expression.
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.transformDynamicProps = transformDynamicProps;
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
const parser_1 = require("../../../lib/parser");
|
|
6
|
+
const utils_2 = require("../../../lib/utils");
|
|
6
7
|
const PAGE_PROPS = 'props';
|
|
7
8
|
// Find all the member access of the prop, and await them
|
|
8
9
|
// e.g. If there's argument `props`, find all the member access of props.<name>.
|
|
@@ -40,7 +41,7 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
|
40
41
|
parentScopeOfMemberAccess.node !== path.node) {
|
|
41
42
|
// If it's not able to convert, add a comment to the prop access to warn the user
|
|
42
43
|
// e.g. the parent scope is sync, await keyword can't be applied
|
|
43
|
-
const comment = ` ${
|
|
44
|
+
const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${propIdName}.${memberProperty.name}' is accessed without awaiting.`;
|
|
44
45
|
(0, utils_1.insertCommentOnce)(member, j, comment);
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
@@ -121,7 +122,7 @@ function commentOnMatchedReExports(root, j) {
|
|
|
121
122
|
specifier.exported.name === 'default')) {
|
|
122
123
|
if (j.Literal.check(path.value.source)) {
|
|
123
124
|
const localName = specifier.local.name;
|
|
124
|
-
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${
|
|
125
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
125
126
|
modified ||= commentInserted;
|
|
126
127
|
}
|
|
127
128
|
else if (path.value.source === null) {
|
|
@@ -134,7 +135,7 @@ function commentOnMatchedReExports(root, j) {
|
|
|
134
135
|
return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
|
|
135
136
|
});
|
|
136
137
|
if (importDeclaration.size() > 0) {
|
|
137
|
-
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${
|
|
138
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
138
139
|
modified ||= commentInserted;
|
|
139
140
|
}
|
|
140
141
|
}
|
|
@@ -410,7 +411,7 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
410
411
|
// find the argument `currentParam`
|
|
411
412
|
const args = callExpression.value.arguments;
|
|
412
413
|
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
413
|
-
const comment = ` ${
|
|
414
|
+
const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
|
|
414
415
|
const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
415
416
|
modified ||= inserted;
|
|
416
417
|
});
|
|
@@ -750,7 +751,7 @@ function commentSpreadProps(path, propsIdentifierName, j) {
|
|
|
750
751
|
const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
|
|
751
752
|
argument: { name: propsIdentifierName },
|
|
752
753
|
});
|
|
753
|
-
const comment = ` ${
|
|
754
|
+
const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
|
|
754
755
|
// Add comment before it
|
|
755
756
|
jsxSpreadProperties.forEach((spread) => {
|
|
756
757
|
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isReactHookName = exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.
|
|
3
|
+
exports.isReactHookName = exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXTJS_ENTRY_FILES = void 0;
|
|
4
4
|
exports.isFunctionType = isFunctionType;
|
|
5
5
|
exports.isMatchedFunctionExported = isMatchedFunctionExported;
|
|
6
6
|
exports.determineClientDirective = determineClientDirective;
|
|
@@ -19,8 +19,7 @@ exports.containsReactHooksCallExpressions = containsReactHooksCallExpressions;
|
|
|
19
19
|
exports.isParentUseCallExpression = isParentUseCallExpression;
|
|
20
20
|
exports.isParentPromiseAllCallExpression = isParentPromiseAllCallExpression;
|
|
21
21
|
exports.NEXTJS_ENTRY_FILES = /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/;
|
|
22
|
-
|
|
23
|
-
const NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore';
|
|
22
|
+
const utils_1 = require("../../../lib/utils");
|
|
24
23
|
exports.TARGET_ROUTE_EXPORTS = new Set([
|
|
25
24
|
'GET',
|
|
26
25
|
'POST',
|
|
@@ -332,13 +331,13 @@ function wrapParentheseIfNeeded(hasChainAccess, j, expression) {
|
|
|
332
331
|
function existsComment(comments, comment) {
|
|
333
332
|
const isCodemodErrorComment = comment
|
|
334
333
|
.trim()
|
|
335
|
-
.startsWith(
|
|
334
|
+
.startsWith(utils_1.NEXT_CODEMOD_ERROR_PREFIX);
|
|
336
335
|
let hasIgnoreComment = false;
|
|
337
336
|
let hasComment = false;
|
|
338
337
|
if (comments) {
|
|
339
338
|
comments.forEach((commentNode) => {
|
|
340
339
|
const currentComment = commentNode.value;
|
|
341
|
-
if (currentComment.trim().startsWith(NEXT_CODEMOD_IGNORE_ERROR_PREFIX)) {
|
|
340
|
+
if (currentComment.trim().startsWith(utils_1.NEXT_CODEMOD_IGNORE_ERROR_PREFIX)) {
|
|
342
341
|
hasIgnoreComment = true;
|
|
343
342
|
}
|
|
344
343
|
if (currentComment === comment) {
|
package/transforms/new-link.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.default = transformer;
|
|
6
6
|
const parser_1 = require("../lib/parser");
|
|
7
|
-
const utils_1 = require("
|
|
7
|
+
const utils_1 = require("../lib/utils");
|
|
8
8
|
function transformer(file, _api) {
|
|
9
9
|
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
10
10
|
const $j = j(file.source);
|