@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.
Files changed (33) hide show
  1. package/bin/next-codemod.js +55 -3
  2. package/bin/shared.js +7 -0
  3. package/bin/transform.js +124 -0
  4. package/bin/upgrade.js +473 -0
  5. package/lib/cra-to-next/global-css-transform.js +5 -5
  6. package/lib/cra-to-next/index-to-component.js +5 -5
  7. package/lib/handle-package.js +110 -0
  8. package/lib/install.js +2 -3
  9. package/lib/parser.js +28 -0
  10. package/lib/run-jscodeshift.js +18 -2
  11. package/lib/utils.js +115 -0
  12. package/package.json +15 -7
  13. package/transforms/add-missing-react-import.js +4 -3
  14. package/transforms/app-dir-runtime-config-experimental-edge.js +34 -0
  15. package/transforms/built-in-next-font.js +4 -3
  16. package/transforms/cra-to-next.js +238 -236
  17. package/transforms/lib/async-request-api/index.js +16 -0
  18. package/transforms/lib/async-request-api/next-async-dynamic-api.js +284 -0
  19. package/transforms/lib/async-request-api/next-async-dynamic-prop.js +713 -0
  20. package/transforms/lib/async-request-api/utils.js +473 -0
  21. package/transforms/metadata-to-viewport-export.js +4 -3
  22. package/transforms/name-default-component.js +6 -6
  23. package/transforms/new-link.js +9 -7
  24. package/transforms/next-async-request-api.js +9 -0
  25. package/transforms/next-dynamic-access-named-export.js +66 -0
  26. package/transforms/next-image-experimental.js +12 -15
  27. package/transforms/next-image-to-legacy-image.js +8 -9
  28. package/transforms/next-og-import.js +4 -3
  29. package/transforms/next-request-geo-ip.js +339 -0
  30. package/transforms/url-to-withrouter.js +1 -1
  31. package/transforms/withamp-to-config.js +1 -1
  32. package/bin/cli.js +0 -216
  33. 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