@next/codemod 15.0.0-rc.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/next-codemod.js +55 -3
- package/bin/shared.js +7 -0
- package/bin/transform.js +124 -0
- package/bin/upgrade.js +473 -0
- package/lib/cra-to-next/global-css-transform.js +5 -5
- package/lib/cra-to-next/index-to-component.js +5 -5
- package/lib/handle-package.js +110 -0
- package/lib/install.js +2 -3
- package/lib/parser.js +28 -0
- package/lib/run-jscodeshift.js +18 -2
- package/lib/utils.js +115 -0
- package/package.json +15 -7
- package/transforms/add-missing-react-import.js +4 -3
- package/transforms/app-dir-runtime-config-experimental-edge.js +34 -0
- package/transforms/built-in-next-font.js +4 -3
- package/transforms/cra-to-next.js +238 -236
- package/transforms/lib/async-request-api/index.js +16 -0
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +284 -0
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +713 -0
- package/transforms/lib/async-request-api/utils.js +473 -0
- package/transforms/metadata-to-viewport-export.js +4 -3
- package/transforms/name-default-component.js +6 -6
- package/transforms/new-link.js +9 -7
- package/transforms/next-async-request-api.js +9 -0
- package/transforms/next-dynamic-access-named-export.js +66 -0
- package/transforms/next-image-experimental.js +12 -15
- package/transforms/next-image-to-legacy-image.js +8 -9
- package/transforms/next-og-import.js +4 -3
- package/transforms/next-request-geo-ip.js +339 -0
- package/transforms/url-to-withrouter.js +1 -1
- package/transforms/withamp-to-config.js +1 -1
- package/bin/cli.js +0 -216
- package/lib/uninstall-package.js +0 -32
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformDynamicAPI = transformDynamicAPI;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const parser_1 = require("../../../lib/parser");
|
|
6
|
+
const DYNAMIC_IMPORT_WARN_COMMENT = ` @next-codemod-error The APIs under 'next/headers' are async now, need to be manually awaited. `;
|
|
7
|
+
function findDynamicImportsAndComment(root, j) {
|
|
8
|
+
let modified = false;
|
|
9
|
+
// find all the dynamic imports of `next/headers`,
|
|
10
|
+
// and add a comment to the import expression to inform this needs to be manually handled
|
|
11
|
+
// find all the dynamic imports of `next/cookies`,
|
|
12
|
+
// Notice, import() is not handled as ImportExpression in current jscodeshift version,
|
|
13
|
+
// we need to use CallExpression to capture the dynamic imports.
|
|
14
|
+
const importPaths = root.find(j.CallExpression, {
|
|
15
|
+
callee: {
|
|
16
|
+
type: 'Import',
|
|
17
|
+
},
|
|
18
|
+
arguments: [{ value: 'next/headers' }],
|
|
19
|
+
});
|
|
20
|
+
importPaths.forEach((path) => {
|
|
21
|
+
const inserted = (0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
22
|
+
modified ||= inserted;
|
|
23
|
+
});
|
|
24
|
+
return modified;
|
|
25
|
+
}
|
|
26
|
+
function transformDynamicAPI(source, _api, filePath) {
|
|
27
|
+
const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
|
|
28
|
+
const j = (0, parser_1.createParserFromPath)(filePath);
|
|
29
|
+
const root = j(source);
|
|
30
|
+
let modified = false;
|
|
31
|
+
// Check if 'use' from 'react' needs to be imported
|
|
32
|
+
let needsReactUseImport = false;
|
|
33
|
+
const insertedTypes = new Set();
|
|
34
|
+
function isImportedInModule(path, functionName) {
|
|
35
|
+
const closestDef = j(path)
|
|
36
|
+
.closestScope()
|
|
37
|
+
.findVariableDeclarators(functionName);
|
|
38
|
+
return closestDef.size() === 0;
|
|
39
|
+
}
|
|
40
|
+
function processAsyncApiCalls(asyncRequestApiName, originRequestApiName) {
|
|
41
|
+
// Process each call to cookies() or headers()
|
|
42
|
+
root
|
|
43
|
+
.find(j.CallExpression, {
|
|
44
|
+
callee: {
|
|
45
|
+
type: 'Identifier',
|
|
46
|
+
name: asyncRequestApiName,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
.forEach((path) => {
|
|
50
|
+
const isImportedTopLevel = isImportedInModule(path, asyncRequestApiName);
|
|
51
|
+
if (!isImportedTopLevel) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let parentFunctionPath = (0, utils_1.findClosetParentFunctionScope)(path, j);
|
|
55
|
+
// We found the parent scope is not a function
|
|
56
|
+
let parentFunctionNode;
|
|
57
|
+
if (parentFunctionPath) {
|
|
58
|
+
if ((0, utils_1.isFunctionScope)(parentFunctionPath, j)) {
|
|
59
|
+
parentFunctionNode = parentFunctionPath.node;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const scopeNode = parentFunctionPath.node;
|
|
63
|
+
if (scopeNode.type === 'ReturnStatement' &&
|
|
64
|
+
(0, utils_1.isFunctionType)(scopeNode.argument.type)) {
|
|
65
|
+
parentFunctionNode = scopeNode.argument;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const isAsyncFunction = parentFunctionNode?.async || false;
|
|
70
|
+
const isCallAwaited = path.parentPath?.node?.type === 'AwaitExpression';
|
|
71
|
+
const hasChainAccess = path.parentPath.value.type === 'MemberExpression' &&
|
|
72
|
+
path.parentPath.value.object === path.node;
|
|
73
|
+
const closetScope = j(path).closestScope();
|
|
74
|
+
// For cookies/headers API, only transform server and shared components
|
|
75
|
+
if (isAsyncFunction) {
|
|
76
|
+
if (!isCallAwaited) {
|
|
77
|
+
// Add 'await' in front of cookies() call
|
|
78
|
+
const expr = j.awaitExpression(
|
|
79
|
+
// add parentheses to wrap the function call
|
|
80
|
+
j.callExpression(j.identifier(asyncRequestApiName), []));
|
|
81
|
+
j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
|
|
82
|
+
modified = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Determine if the function is an export
|
|
87
|
+
const closetScopePath = closetScope.get();
|
|
88
|
+
const isEntryFileExport = isEntryFile && (0, utils_1.isMatchedFunctionExported)(closetScopePath, j);
|
|
89
|
+
const closestFunctionNode = closetScope.size()
|
|
90
|
+
? closetScopePath.node
|
|
91
|
+
: null;
|
|
92
|
+
// If it's exporting a function directly, exportFunctionNode is same as exportNode
|
|
93
|
+
// e.g. export default function MyComponent() {}
|
|
94
|
+
// If it's exporting a variable declaration, exportFunctionNode is the function declaration
|
|
95
|
+
// e.g. export const MyComponent = function() {}
|
|
96
|
+
let exportFunctionNode;
|
|
97
|
+
if (isEntryFileExport) {
|
|
98
|
+
if (closestFunctionNode &&
|
|
99
|
+
(0, utils_1.isFunctionType)(closestFunctionNode.type)) {
|
|
100
|
+
exportFunctionNode = closestFunctionNode;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Is normal async function
|
|
105
|
+
exportFunctionNode = closestFunctionNode;
|
|
106
|
+
}
|
|
107
|
+
let canConvertToAsync = false;
|
|
108
|
+
// check if current path is under the default export function
|
|
109
|
+
if (isEntryFileExport) {
|
|
110
|
+
// if default export function is not async, convert it to async, and await the api call
|
|
111
|
+
if (!isCallAwaited && (0, utils_1.isFunctionType)(exportFunctionNode.type)) {
|
|
112
|
+
const hasReactHooksUsage = (0, utils_1.containsReactHooksCallExpressions)(closetScopePath, j);
|
|
113
|
+
// If the scoped function is async function
|
|
114
|
+
if (exportFunctionNode.async === false && !hasReactHooksUsage) {
|
|
115
|
+
canConvertToAsync = true;
|
|
116
|
+
exportFunctionNode.async = true;
|
|
117
|
+
}
|
|
118
|
+
if (canConvertToAsync) {
|
|
119
|
+
const expr = j.awaitExpression(j.callExpression(j.identifier(asyncRequestApiName), []));
|
|
120
|
+
j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
|
|
121
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(closetScopePath.node, j);
|
|
122
|
+
modified = true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// If it's still sync function that cannot be converted to async, wrap the api call with 'use()' if needed
|
|
126
|
+
if (!(0, utils_1.isParentUseCallExpression)(path, j)) {
|
|
127
|
+
j(path).replaceWith(j.callExpression(j.identifier('use'), [
|
|
128
|
+
j.callExpression(j.identifier(asyncRequestApiName), []),
|
|
129
|
+
]));
|
|
130
|
+
needsReactUseImport = true;
|
|
131
|
+
modified = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// if parent is function and it's a hook, which starts with 'use', wrap the api call with 'use()'
|
|
138
|
+
const parentFunction = (0, utils_1.findClosetParentFunctionScope)(path, j);
|
|
139
|
+
if (parentFunction) {
|
|
140
|
+
const parentFunctionName = parentFunction.get().node.id?.name || '';
|
|
141
|
+
const isParentFunctionHook = (0, utils_1.isReactHookName)(parentFunctionName);
|
|
142
|
+
if (isParentFunctionHook && !(0, utils_1.isParentUseCallExpression)(path, j)) {
|
|
143
|
+
j(path).replaceWith(j.callExpression(j.identifier('use'), [
|
|
144
|
+
j.callExpression(j.identifier(asyncRequestApiName), []),
|
|
145
|
+
]));
|
|
146
|
+
needsReactUseImport = true;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} Manually await this call and refactor the function to be async `);
|
|
150
|
+
modified ||= casted;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} please manually await this call, codemod cannot transform due to undetermined async scope `);
|
|
155
|
+
modified ||= casted;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Handle type usage of async API, e.g. `type Cookie = ReturnType<typeof cookies>`
|
|
161
|
+
// convert it to `type Cookie = Awaited<ReturnType<typeof cookies>>`
|
|
162
|
+
root
|
|
163
|
+
.find(j.TSTypeReference, {
|
|
164
|
+
typeName: {
|
|
165
|
+
type: 'Identifier',
|
|
166
|
+
name: 'ReturnType',
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
.forEach((path) => {
|
|
170
|
+
const typeParam = path.node.typeParameters?.params[0];
|
|
171
|
+
// Check if the ReturnType is for 'cookies'
|
|
172
|
+
if (typeParam &&
|
|
173
|
+
j.TSTypeQuery.check(typeParam) &&
|
|
174
|
+
j.Identifier.check(typeParam.exprName) &&
|
|
175
|
+
typeParam.exprName.name === asyncRequestApiName) {
|
|
176
|
+
// Replace ReturnType<typeof cookies> with Awaited<ReturnType<typeof cookies>>
|
|
177
|
+
const awaitedTypeReference = j.tsTypeReference(j.identifier('Awaited'), j.tsTypeParameterInstantiation([
|
|
178
|
+
j.tsTypeReference(j.identifier('ReturnType'), j.tsTypeParameterInstantiation([typeParam])),
|
|
179
|
+
]));
|
|
180
|
+
j(path).replaceWith(awaitedTypeReference);
|
|
181
|
+
modified = true;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const isClientComponent = (0, utils_1.determineClientDirective)(root, j);
|
|
186
|
+
// Only transform the valid calls in server or shared components
|
|
187
|
+
if (isClientComponent)
|
|
188
|
+
return null;
|
|
189
|
+
// Import declaration case, e.g. import { cookies } from 'next/headers'
|
|
190
|
+
const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
|
|
191
|
+
for (const originName in importedNextAsyncRequestApisMapping) {
|
|
192
|
+
const aliasName = importedNextAsyncRequestApisMapping[originName];
|
|
193
|
+
processAsyncApiCalls(aliasName, originName);
|
|
194
|
+
}
|
|
195
|
+
// Add import { use } from 'react' if needed and not already imported
|
|
196
|
+
if (needsReactUseImport) {
|
|
197
|
+
(0, utils_1.insertReactUseImport)(root, j);
|
|
198
|
+
}
|
|
199
|
+
const commented = findDynamicImportsAndComment(root, j);
|
|
200
|
+
modified ||= commented;
|
|
201
|
+
return modified ? root.toSource() : null;
|
|
202
|
+
}
|
|
203
|
+
// cast to unknown first, then the specific type
|
|
204
|
+
const API_CAST_TYPE_MAP = {
|
|
205
|
+
cookies: 'UnsafeUnwrappedCookies',
|
|
206
|
+
headers: 'UnsafeUnwrappedHeaders',
|
|
207
|
+
draftMode: 'UnsafeUnwrappedDraftMode',
|
|
208
|
+
};
|
|
209
|
+
function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
|
|
210
|
+
let modified = false;
|
|
211
|
+
const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
212
|
+
if (isTsFile) {
|
|
213
|
+
// if the path of call expression is already being awaited, no need to cast
|
|
214
|
+
if (path.parentPath?.node?.type === 'AwaitExpression')
|
|
215
|
+
return false;
|
|
216
|
+
/* Do type cast for headers, cookies, draftMode
|
|
217
|
+
import {
|
|
218
|
+
type UnsafeUnwrappedHeaders,
|
|
219
|
+
type UnsafeUnwrappedCookies,
|
|
220
|
+
type UnsafeUnwrappedDraftMode
|
|
221
|
+
} from 'next/headers'
|
|
222
|
+
|
|
223
|
+
cookies() as unknown as UnsafeUnwrappedCookies
|
|
224
|
+
headers() as unknown as UnsafeUnwrappedHeaders
|
|
225
|
+
draftMode() as unknown as UnsafeUnwrappedDraftMode
|
|
226
|
+
|
|
227
|
+
e.g. `<path>` is cookies(), convert it to `(<path> as unknown as UnsafeUnwrappedCookies)`
|
|
228
|
+
*/
|
|
229
|
+
const targetType = API_CAST_TYPE_MAP[originRequestApiName];
|
|
230
|
+
const newCastExpression = j.tsAsExpression(j.tsAsExpression(path.node, j.tsUnknownKeyword()), j.tsTypeReference(j.identifier(targetType)));
|
|
231
|
+
// Replace the original expression with the new cast expression,
|
|
232
|
+
// also wrap () around the new cast expression.
|
|
233
|
+
j(path).replaceWith(j.parenthesizedExpression(newCastExpression));
|
|
234
|
+
modified = true;
|
|
235
|
+
// If cast types are not imported, add them to the import list
|
|
236
|
+
const importDeclaration = root.find(j.ImportDeclaration, {
|
|
237
|
+
source: { value: 'next/headers' },
|
|
238
|
+
});
|
|
239
|
+
if (importDeclaration.size() > 0) {
|
|
240
|
+
const hasImportedType = importDeclaration
|
|
241
|
+
.find(j.TSTypeAliasDeclaration, {
|
|
242
|
+
id: { name: targetType },
|
|
243
|
+
})
|
|
244
|
+
.size() > 0 ||
|
|
245
|
+
importDeclaration
|
|
246
|
+
.find(j.ImportSpecifier, {
|
|
247
|
+
imported: { name: targetType },
|
|
248
|
+
})
|
|
249
|
+
.size() > 0;
|
|
250
|
+
if (!hasImportedType && !insertedTypes.has(targetType)) {
|
|
251
|
+
importDeclaration
|
|
252
|
+
.get()
|
|
253
|
+
.node.specifiers.push(j.importSpecifier(j.identifier(`type ${targetType}`)));
|
|
254
|
+
insertedTypes.add(targetType);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Otherwise for JS file, leave a message to the user to manually handle the transformation
|
|
260
|
+
const inserted = (0, utils_1.insertCommentOnce)(path.node, j, customMessage);
|
|
261
|
+
modified ||= inserted;
|
|
262
|
+
}
|
|
263
|
+
return modified;
|
|
264
|
+
}
|
|
265
|
+
function findImportMappingFromNextHeaders(root, j) {
|
|
266
|
+
const mappings = {};
|
|
267
|
+
// Find the import declaration from 'next/headers'
|
|
268
|
+
root
|
|
269
|
+
.find(j.ImportDeclaration, { source: { value: 'next/headers' } })
|
|
270
|
+
.forEach((importPath) => {
|
|
271
|
+
const importDeclaration = importPath.node;
|
|
272
|
+
// Iterate over the specifiers and build the mappings
|
|
273
|
+
importDeclaration.specifiers.forEach((specifier) => {
|
|
274
|
+
if (j.ImportSpecifier.check(specifier)) {
|
|
275
|
+
const importedName = specifier.imported.name; // Original name (e.g., cookies)
|
|
276
|
+
const localName = specifier.local.name; // Local name (e.g., myCookies or same as importedName)
|
|
277
|
+
// Add to the mappings
|
|
278
|
+
mappings[importedName] = localName;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
return mappings;
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=next-async-dynamic-api.js.map
|