@next/codemod 15.0.0-rc.0 → 15.0.0-rc.1

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 +390 -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 +13 -6
  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 +274 -0
  19. package/transforms/lib/async-request-api/next-async-dynamic-prop.js +715 -0
  20. package/transforms/lib/async-request-api/utils.js +374 -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,274 @@
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) {
112
+ // If the scoped function is async function
113
+ if ((0, utils_1.isFunctionType)(exportFunctionNode.type) &&
114
+ exportFunctionNode.async === false) {
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
+ }
125
+ }
126
+ else {
127
+ // if parent is function and it's a hook, which starts with 'use', wrap the api call with 'use()'
128
+ const parentFunction = (0, utils_1.findClosetParentFunctionScope)(path, j);
129
+ if (parentFunction) {
130
+ const parentFunctionName = parentFunction.get().node.id?.name;
131
+ const isParentFunctionHook = parentFunctionName?.startsWith('use');
132
+ if (isParentFunctionHook) {
133
+ j(path).replaceWith(j.callExpression(j.identifier('use'), [
134
+ j.callExpression(j.identifier(asyncRequestApiName), []),
135
+ ]));
136
+ needsReactUseImport = true;
137
+ }
138
+ else {
139
+ 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 `);
140
+ modified ||= casted;
141
+ }
142
+ }
143
+ else {
144
+ 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 `);
145
+ modified ||= casted;
146
+ }
147
+ }
148
+ }
149
+ });
150
+ // Handle type usage of async API, e.g. `type Cookie = ReturnType<typeof cookies>`
151
+ // convert it to `type Cookie = Awaited<ReturnType<typeof cookies>>`
152
+ root
153
+ .find(j.TSTypeReference, {
154
+ typeName: {
155
+ type: 'Identifier',
156
+ name: 'ReturnType',
157
+ },
158
+ })
159
+ .forEach((path) => {
160
+ const typeParam = path.node.typeParameters?.params[0];
161
+ // Check if the ReturnType is for 'cookies'
162
+ if (typeParam &&
163
+ j.TSTypeQuery.check(typeParam) &&
164
+ j.Identifier.check(typeParam.exprName) &&
165
+ typeParam.exprName.name === asyncRequestApiName) {
166
+ // Replace ReturnType<typeof cookies> with Awaited<ReturnType<typeof cookies>>
167
+ const awaitedTypeReference = j.tsTypeReference(j.identifier('Awaited'), j.tsTypeParameterInstantiation([
168
+ j.tsTypeReference(j.identifier('ReturnType'), j.tsTypeParameterInstantiation([typeParam])),
169
+ ]));
170
+ j(path).replaceWith(awaitedTypeReference);
171
+ modified = true;
172
+ }
173
+ });
174
+ }
175
+ const isClientComponent = (0, utils_1.determineClientDirective)(root, j);
176
+ // Only transform the valid calls in server or shared components
177
+ if (isClientComponent)
178
+ return null;
179
+ // Import declaration case, e.g. import { cookies } from 'next/headers'
180
+ const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
181
+ for (const originName in importedNextAsyncRequestApisMapping) {
182
+ const aliasName = importedNextAsyncRequestApisMapping[originName];
183
+ processAsyncApiCalls(aliasName, originName);
184
+ }
185
+ // Add import { use } from 'react' if needed and not already imported
186
+ if (needsReactUseImport) {
187
+ (0, utils_1.insertReactUseImport)(root, j);
188
+ }
189
+ const commented = findDynamicImportsAndComment(root, j);
190
+ modified ||= commented;
191
+ return modified ? root.toSource() : null;
192
+ }
193
+ // cast to unknown first, then the specific type
194
+ const API_CAST_TYPE_MAP = {
195
+ cookies: 'UnsafeUnwrappedCookies',
196
+ headers: 'UnsafeUnwrappedHeaders',
197
+ draftMode: 'UnsafeUnwrappedDraftMode',
198
+ };
199
+ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
200
+ let modified = false;
201
+ const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
202
+ if (isTsFile) {
203
+ // if the path of call expression is already being awaited, no need to cast
204
+ if (path.parentPath?.node?.type === 'AwaitExpression')
205
+ return false;
206
+ /* Do type cast for headers, cookies, draftMode
207
+ import {
208
+ type UnsafeUnwrappedHeaders,
209
+ type UnsafeUnwrappedCookies,
210
+ type UnsafeUnwrappedDraftMode
211
+ } from 'next/headers'
212
+
213
+ cookies() as unknown as UnsafeUnwrappedCookies
214
+ headers() as unknown as UnsafeUnwrappedHeaders
215
+ draftMode() as unknown as UnsafeUnwrappedDraftMode
216
+
217
+ e.g. `<path>` is cookies(), convert it to `(<path> as unknown as UnsafeUnwrappedCookies)`
218
+ */
219
+ const targetType = API_CAST_TYPE_MAP[originRequestApiName];
220
+ const newCastExpression = j.tsAsExpression(j.tsAsExpression(path.node, j.tsUnknownKeyword()), j.tsTypeReference(j.identifier(targetType)));
221
+ // Replace the original expression with the new cast expression,
222
+ // also wrap () around the new cast expression.
223
+ j(path).replaceWith(j.parenthesizedExpression(newCastExpression));
224
+ modified = true;
225
+ // If cast types are not imported, add them to the import list
226
+ const importDeclaration = root.find(j.ImportDeclaration, {
227
+ source: { value: 'next/headers' },
228
+ });
229
+ if (importDeclaration.size() > 0) {
230
+ const hasImportedType = importDeclaration
231
+ .find(j.TSTypeAliasDeclaration, {
232
+ id: { name: targetType },
233
+ })
234
+ .size() > 0 ||
235
+ importDeclaration
236
+ .find(j.ImportSpecifier, {
237
+ imported: { name: targetType },
238
+ })
239
+ .size() > 0;
240
+ if (!hasImportedType && !insertedTypes.has(targetType)) {
241
+ importDeclaration
242
+ .get()
243
+ .node.specifiers.push(j.importSpecifier(j.identifier(`type ${targetType}`)));
244
+ insertedTypes.add(targetType);
245
+ }
246
+ }
247
+ }
248
+ else {
249
+ // Otherwise for JS file, leave a message to the user to manually handle the transformation
250
+ const inserted = (0, utils_1.insertCommentOnce)(path.node, j, customMessage);
251
+ modified ||= inserted;
252
+ }
253
+ return modified;
254
+ }
255
+ function findImportMappingFromNextHeaders(root, j) {
256
+ const mappings = {};
257
+ // Find the import declaration from 'next/headers'
258
+ root
259
+ .find(j.ImportDeclaration, { source: { value: 'next/headers' } })
260
+ .forEach((importPath) => {
261
+ const importDeclaration = importPath.node;
262
+ // Iterate over the specifiers and build the mappings
263
+ importDeclaration.specifiers.forEach((specifier) => {
264
+ if (j.ImportSpecifier.check(specifier)) {
265
+ const importedName = specifier.imported.name; // Original name (e.g., cookies)
266
+ const localName = specifier.local.name; // Local name (e.g., myCookies or same as importedName)
267
+ // Add to the mappings
268
+ mappings[importedName] = localName;
269
+ }
270
+ });
271
+ });
272
+ return mappings;
273
+ }
274
+ //# sourceMappingURL=next-async-dynamic-api.js.map