@next/codemod 15.0.0-canary.18 → 15.0.0-canary.182

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