@next/codemod 15.0.0-canary.171 → 15.0.0-canary.173

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.0.0-canary.171",
3
+ "version": "15.0.0-canary.173",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,8 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicAPI = transformDynamicAPI;
4
4
  const utils_1 = require("./utils");
5
- function wrapParathnessIfNeeded(hasChainAccess, j, expression) {
6
- return hasChainAccess ? j.parenthesizedExpression(expression) : expression;
5
+ const DYNAMIC_IMPORT_WARN_COMMENT = ` 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
+ (0, utils_1.insertCommentOnce)(path, j, DYNAMIC_IMPORT_WARN_COMMENT);
21
+ modified = true;
22
+ });
23
+ return modified;
7
24
  }
8
25
  function transformDynamicAPI(source, api, filePath) {
9
26
  const j = api.jscodeshift.withParser('tsx');
@@ -32,22 +49,26 @@ function transformDynamicAPI(source, api, filePath) {
32
49
  if (!isImportedTopLevel) {
33
50
  return;
34
51
  }
35
- const closetScope = j(path).closestScope();
36
- // First search the closed function of the current path
37
- let closestFunction;
38
- closestFunction = j(path).closest(j.FunctionDeclaration);
39
- if (closestFunction.size() === 0) {
40
- closestFunction = j(path).closest(j.FunctionExpression);
41
- }
42
- if (closestFunction.size() === 0) {
43
- closestFunction = j(path).closest(j.ArrowFunctionExpression);
52
+ let parentFunctionPath = (0, utils_1.findClosetParentFunctionScope)(path, j);
53
+ // We found the parent scope is not a function
54
+ let parentFunctionNode;
55
+ if (parentFunctionPath) {
56
+ if ((0, utils_1.isFunctionScope)(parentFunctionPath, j)) {
57
+ parentFunctionNode = parentFunctionPath.node;
58
+ }
59
+ else {
60
+ const scopeNode = parentFunctionPath.node;
61
+ if (scopeNode.type === 'ReturnStatement' &&
62
+ (0, utils_1.isFunctionType)(scopeNode.argument.type)) {
63
+ parentFunctionNode = scopeNode.argument;
64
+ }
65
+ }
44
66
  }
45
- const isAsyncFunction = closestFunction
46
- .nodes()
47
- .some((node) => node.async);
67
+ const isAsyncFunction = parentFunctionNode?.async || false;
48
68
  const isCallAwaited = path.parentPath?.node?.type === 'AwaitExpression';
49
69
  const hasChainAccess = path.parentPath.value.type === 'MemberExpression' &&
50
70
  path.parentPath.value.object === path.node;
71
+ const closetScope = j(path).closestScope();
51
72
  // For cookies/headers API, only transform server and shared components
52
73
  if (isAsyncFunction) {
53
74
  if (!isCallAwaited) {
@@ -55,7 +76,7 @@ function transformDynamicAPI(source, api, filePath) {
55
76
  const expr = j.awaitExpression(
56
77
  // add parentheses to wrap the function call
57
78
  j.callExpression(j.identifier(asyncRequestApiName), []));
58
- j(path).replaceWith(wrapParathnessIfNeeded(hasChainAccess, j, expr));
79
+ j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
59
80
  modified = true;
60
81
  }
61
82
  }
@@ -94,7 +115,7 @@ function transformDynamicAPI(source, api, filePath) {
94
115
  }
95
116
  if (canConvertToAsync) {
96
117
  const expr = j.awaitExpression(j.callExpression(j.identifier(asyncRequestApiName), []));
97
- j(path).replaceWith(wrapParathnessIfNeeded(hasChainAccess, j, expr));
118
+ j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
98
119
  (0, utils_1.turnFunctionReturnTypeToAsync)(closetScopePath.node, j);
99
120
  modified = true;
100
121
  }
@@ -102,10 +123,8 @@ function transformDynamicAPI(source, api, filePath) {
102
123
  }
103
124
  else {
104
125
  // if parent is function and it's a hook, which starts with 'use', wrap the api call with 'use()'
105
- const parentFunction = j(path).closest(j.FunctionDeclaration) ||
106
- j(path).closest(j.FunctionExpression) ||
107
- j(path).closest(j.ArrowFunctionExpression);
108
- if (parentFunction.size() > 0) {
126
+ const parentFunction = (0, utils_1.findClosetParentFunctionScope)(path, j);
127
+ if (parentFunction) {
109
128
  const parentFunctionName = parentFunction.get().node.id?.name;
110
129
  const isParentFunctionHook = parentFunctionName?.startsWith('use');
111
130
  if (isParentFunctionHook) {
@@ -115,11 +134,11 @@ function transformDynamicAPI(source, api, filePath) {
115
134
  needsReactUseImport = true;
116
135
  }
117
136
  else {
118
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes);
137
+ castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` TODO: please manually await this call, if it's a server component, you can turn it to async function `);
119
138
  }
120
139
  }
121
140
  else {
122
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes);
141
+ castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' TODO: please manually await this call, codemod cannot transform due to undetermined async scope ');
123
142
  }
124
143
  modified = true;
125
144
  }
@@ -127,17 +146,21 @@ function transformDynamicAPI(source, api, filePath) {
127
146
  });
128
147
  }
129
148
  const isClientComponent = (0, utils_1.determineClientDirective)(root, j, source);
130
- const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
131
149
  // Only transform the valid calls in server or shared components
132
- if (!isClientComponent) {
133
- for (const originName in importedNextAsyncRequestApisMapping) {
134
- const aliasName = importedNextAsyncRequestApisMapping[originName];
135
- processAsyncApiCalls(aliasName, originName);
136
- }
137
- // Add import { use } from 'react' if needed and not already imported
138
- if (needsReactUseImport) {
139
- (0, utils_1.insertReactUseImport)(root, j);
140
- }
150
+ if (isClientComponent)
151
+ return null;
152
+ // Import declaration case, e.g. import { cookies } from 'next/headers'
153
+ const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
154
+ for (const originName in importedNextAsyncRequestApisMapping) {
155
+ const aliasName = importedNextAsyncRequestApisMapping[originName];
156
+ processAsyncApiCalls(aliasName, originName);
157
+ }
158
+ // Add import { use } from 'react' if needed and not already imported
159
+ if (needsReactUseImport) {
160
+ (0, utils_1.insertReactUseImport)(root, j);
161
+ }
162
+ if (findDynamicImportsAndComment(root, j)) {
163
+ modified = true;
141
164
  }
142
165
  return modified ? root.toSource() : null;
143
166
  }
@@ -147,9 +170,12 @@ const API_CAST_TYPE_MAP = {
147
170
  headers: 'UnsafeUnwrappedHeaders',
148
171
  draftMode: 'UnsafeUnwrappedDraftMode',
149
172
  };
150
- function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes) {
173
+ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
151
174
  const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
152
175
  if (isTsFile) {
176
+ // if the path of call expression is already being awaited, no need to cast
177
+ if (path.parentPath?.node?.type === 'AwaitExpression')
178
+ return;
153
179
  /* Do type cast for headers, cookies, draftMode
154
180
  import {
155
181
  type UnsafeUnwrappedHeaders,
@@ -194,7 +220,7 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
194
220
  else {
195
221
  // Otherwise for JS file, leave a message to the user to manually handle the transformation
196
222
  path.node.comments = [
197
- j.commentBlock(' TODO: please manually await this call, codemod cannot transform due to undetermined async scope '),
223
+ j.commentBlock(customMessage),
198
224
  ...(path.node.comments || []),
199
225
  ];
200
226
  }
@@ -3,16 +3,95 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicProps = transformDynamicProps;
4
4
  const utils_1 = require("./utils");
5
5
  const PAGE_PROPS = 'props';
6
- function isAsyncFunctionDeclaration(path) {
7
- const decl = path.value.declaration;
8
- const isAsyncFunction = (decl.type === 'FunctionDeclaration' ||
9
- decl.type === 'FunctionExpression' ||
10
- decl.type === 'ArrowFunctionExpression') &&
11
- decl.async;
12
- return isAsyncFunction;
6
+ function findFunctionBody(path) {
7
+ let functionBody = path.node.body;
8
+ if (functionBody && functionBody.type === 'BlockStatement') {
9
+ return functionBody.body;
10
+ }
11
+ return null;
12
+ }
13
+ function awaitMemberAccessOfProp(propIdName, path, j) {
14
+ // search the member access of the prop
15
+ const functionBody = findFunctionBody(path);
16
+ const memberAccess = j(functionBody).find(j.MemberExpression, {
17
+ object: {
18
+ type: 'Identifier',
19
+ name: propIdName,
20
+ },
21
+ });
22
+ let hasAwaited = false;
23
+ // await each member access
24
+ memberAccess.forEach((memberAccessPath) => {
25
+ const member = memberAccessPath.value;
26
+ // check if it's already awaited
27
+ if (memberAccessPath.parentPath?.value.type === 'AwaitExpression') {
28
+ return;
29
+ }
30
+ const awaitedExpr = j.awaitExpression(member);
31
+ const awaitMemberAccess = (0, utils_1.wrapParentheseIfNeeded)(true, j, awaitedExpr);
32
+ memberAccessPath.replace(awaitMemberAccess);
33
+ hasAwaited = true;
34
+ });
35
+ // If there's any awaited member access, we need to make the function async
36
+ if (hasAwaited) {
37
+ if (!path.value.async) {
38
+ if ('async' in path.value) {
39
+ path.value.async = true;
40
+ (0, utils_1.turnFunctionReturnTypeToAsync)(path.value, j);
41
+ }
42
+ }
43
+ }
44
+ return hasAwaited;
45
+ }
46
+ function applyUseAndRenameAccessedProp(propIdName, path, j) {
47
+ // search the member access of the prop, and rename the member access to the member value
48
+ // e.g.
49
+ // props.params => params
50
+ // props.params.foo => params.foo
51
+ // props.searchParams.search => searchParams.search
52
+ let modified = false;
53
+ const functionBody = findFunctionBody(path);
54
+ const memberAccess = j(functionBody).find(j.MemberExpression, {
55
+ object: {
56
+ type: 'Identifier',
57
+ name: propIdName,
58
+ },
59
+ });
60
+ const accessedNames = [];
61
+ // rename each member access
62
+ memberAccess.forEach((memberAccessPath) => {
63
+ const member = memberAccessPath.value;
64
+ const memberProperty = member.property;
65
+ if (j.Identifier.check(memberProperty)) {
66
+ accessedNames.push(memberProperty.name);
67
+ }
68
+ else if (j.MemberExpression.check(memberProperty)) {
69
+ let currentMember = memberProperty;
70
+ if (j.Identifier.check(currentMember.object)) {
71
+ accessedNames.push(currentMember.object.name);
72
+ }
73
+ }
74
+ memberAccessPath.replace(memberProperty);
75
+ });
76
+ // If there's any renamed member access, need to call `use()` onto member access
77
+ // e.g. ['params'] => insert `const params = use(props.params)`
78
+ if (accessedNames.length > 0) {
79
+ const accessedPropId = j.identifier(propIdName);
80
+ const accessedProp = j.memberExpression(accessedPropId, j.identifier(accessedNames[0]));
81
+ const useCall = j.callExpression(j.identifier('use'), [accessedProp]);
82
+ const useDeclaration = j.variableDeclaration('const', [
83
+ j.variableDeclarator(j.identifier(accessedNames[0]), useCall),
84
+ ]);
85
+ if (functionBody) {
86
+ functionBody.unshift(useDeclaration);
87
+ }
88
+ modified = true;
89
+ }
90
+ return modified;
13
91
  }
14
92
  function transformDynamicProps(source, api, _filePath) {
15
93
  let modified = false;
94
+ let modifiedPropArgument = false;
16
95
  const j = api.jscodeshift.withParser('tsx');
17
96
  const root = j(source);
18
97
  // Check if 'use' from 'react' needs to be imported
@@ -25,18 +104,22 @@ function transformDynamicProps(source, api, _filePath) {
25
104
  let insertedRenamedPropFunctionNames = new Set();
26
105
  function processAsyncPropOfEntryFile(isClientComponent) {
27
106
  // find `params` and `searchParams` in file, and transform the access to them
28
- function renameAsyncPropIfExisted(path) {
29
- const decl = path.value.declaration;
30
- if (decl.type !== 'FunctionDeclaration' &&
31
- decl.type !== 'FunctionExpression' &&
32
- decl.type !== 'ArrowFunctionExpression') {
33
- return;
34
- }
107
+ function renameAsyncPropIfExisted(path, isDefaultExport) {
108
+ const decl = path.value;
35
109
  const params = decl.params;
110
+ const functionName = decl.id?.name || 'default';
111
+ // target properties mapping, only contains `params` and `searchParams`
36
112
  const propertiesMap = new Map();
37
- // If there's no first param, return
38
- if (params.length !== 1) {
39
- return;
113
+ let allProperties = [];
114
+ // generateMetadata API has 2 params
115
+ if (functionName === 'generateMetadata') {
116
+ if (params.length > 2 || params.length === 0)
117
+ return;
118
+ }
119
+ else {
120
+ // Page/Layout/Route handlers have 1 param
121
+ if (params.length !== 1)
122
+ return;
40
123
  }
41
124
  const propsIdentifier = (0, utils_1.generateUniqueIdentifier)(PAGE_PROPS, path, j);
42
125
  const currentParam = params[0];
@@ -44,19 +127,25 @@ function transformDynamicProps(source, api, _filePath) {
44
127
  if (currentParam.type === 'ObjectPattern') {
45
128
  // Validate if the properties are not `params` and `searchParams`,
46
129
  // if they are, quit the transformation
130
+ let foundTargetProp = false;
47
131
  for (const prop of currentParam.properties) {
48
132
  if ('key' in prop && prop.key.type === 'Identifier') {
49
133
  const propName = prop.key.name;
50
- if (!utils_1.TARGET_PROP_NAMES.has(propName)) {
51
- return;
134
+ if (utils_1.TARGET_PROP_NAMES.has(propName)) {
135
+ foundTargetProp = true;
52
136
  }
53
137
  }
54
138
  }
139
+ // If there's no `params` or `searchParams` matched, return
140
+ if (!foundTargetProp)
141
+ return;
142
+ allProperties = currentParam.properties;
55
143
  currentParam.properties.forEach((prop) => {
56
144
  if (
57
145
  // Could be `Property` or `ObjectProperty`
58
146
  'key' in prop &&
59
- prop.key.type === 'Identifier') {
147
+ prop.key.type === 'Identifier' &&
148
+ utils_1.TARGET_PROP_NAMES.has(prop.key.name)) {
60
149
  const value = 'value' in prop ? prop.value : null;
61
150
  propertiesMap.set(prop.key.name, value);
62
151
  }
@@ -141,41 +230,139 @@ function transformDynamicProps(source, api, _filePath) {
141
230
  // Override the first param to `props`
142
231
  params[0] = propsIdentifier;
143
232
  modified = true;
233
+ modifiedPropArgument = true;
144
234
  }
145
- if (modified) {
146
- resolveAsyncProp(path, propertiesMap, propsIdentifier.name);
147
- }
148
- }
149
- function getBodyOfFunctionDeclaration(path) {
150
- const decl = path.value.declaration;
151
- let functionBody;
152
- if (decl.type === 'FunctionDeclaration' ||
153
- decl.type === 'FunctionExpression' ||
154
- decl.type === 'ArrowFunctionExpression') {
155
- if (decl.body && decl.body.type === 'BlockStatement') {
156
- functionBody = decl.body.body;
235
+ else if (currentParam.type === 'Identifier') {
236
+ // case of accessing the props.params.<name>:
237
+ // Page(props) {}
238
+ // generateMetadata(props, parent?) {}
239
+ const argName = currentParam.name;
240
+ if (isClientComponent) {
241
+ const modifiedProp = applyUseAndRenameAccessedProp(argName, path, j);
242
+ if (modifiedProp) {
243
+ needsReactUseImport = true;
244
+ modified = true;
245
+ }
246
+ }
247
+ else {
248
+ modified = awaitMemberAccessOfProp(argName, path, j);
157
249
  }
250
+ // cases of passing down `props` into any function
251
+ // Page(props) { callback(props) }
252
+ // search for all the argument of CallExpression, where currentParam is one of the arguments
253
+ const callExpressions = j(path).find(j.CallExpression, {
254
+ arguments: (args) => {
255
+ return args.some((arg) => {
256
+ return (j.Identifier.check(arg) &&
257
+ arg.name === argName &&
258
+ arg.type === 'Identifier');
259
+ });
260
+ },
261
+ });
262
+ // Add a comment to warn users that properties of `props` need to be awaited when accessed
263
+ callExpressions.forEach((callExpression) => {
264
+ // find the argument `currentParam`
265
+ const args = callExpression.value.arguments;
266
+ const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
267
+ // insert a comment to the argument
268
+ const comment = j.commentBlock(` '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `, true, false);
269
+ propPassedAsArg.comments = [
270
+ comment,
271
+ ...(propPassedAsArg.comments || []),
272
+ ];
273
+ modified = true;
274
+ });
275
+ }
276
+ if (modifiedPropArgument) {
277
+ resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
158
278
  }
159
- return functionBody;
160
279
  }
161
280
  // Helper function to insert `const params = await asyncParams;` at the beginning of the function body
162
- function resolveAsyncProp(path, propertiesMap, propsIdentifierName) {
163
- const isDefaultExport = path.value.type === 'ExportDefaultDeclaration';
281
+ function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
282
+ const node = path.value;
164
283
  // If it's sync default export, and it's also server component, make the function async
165
- if (isDefaultExport &&
166
- !isClientComponent &&
167
- !isAsyncFunctionDeclaration(path)) {
168
- if ('async' in path.value.declaration) {
169
- path.value.declaration.async = true;
170
- (0, utils_1.turnFunctionReturnTypeToAsync)(path.value.declaration, j);
284
+ if (isDefaultExport && !isClientComponent) {
285
+ if (!node.async) {
286
+ if ('async' in node) {
287
+ node.async = true;
288
+ (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
289
+ }
171
290
  }
172
291
  }
173
- const isAsyncFunc = isAsyncFunctionDeclaration(path);
174
- // @ts-ignore quick way to check if it's a function and it has a name
175
- const functionName = path.value.declaration.id?.name || 'default';
176
- const functionBody = getBodyOfFunctionDeclaration(path);
177
- for (const [propName, paramsProperty] of propertiesMap) {
178
- const propNameIdentifier = j.identifier(propName);
292
+ const isAsyncFunc = !!node.async;
293
+ const functionName = path.value.id?.name || 'default';
294
+ const functionBody = findFunctionBody(path);
295
+ const hasOtherProperties = allProperties.length > propertiesMap.size;
296
+ function createDestructuringDeclaration(properties, destructPropsIdentifierName) {
297
+ const propsToKeep = [];
298
+ let restProperty = null;
299
+ // Iterate over the destructured properties
300
+ properties.forEach((property) => {
301
+ if (j.ObjectProperty.check(property)) {
302
+ // Handle normal and computed properties
303
+ const keyName = j.Identifier.check(property.key)
304
+ ? property.key.name
305
+ : j.Literal.check(property.key)
306
+ ? property.key.value
307
+ : null; // for computed properties
308
+ if (typeof keyName === 'string') {
309
+ propsToKeep.push(property);
310
+ }
311
+ }
312
+ else if (j.RestElement.check(property)) {
313
+ restProperty = property;
314
+ }
315
+ });
316
+ if (propsToKeep.length === 0 && !restProperty) {
317
+ return null;
318
+ }
319
+ if (restProperty) {
320
+ propsToKeep.push(restProperty);
321
+ }
322
+ return j.variableDeclaration('const', [
323
+ j.variableDeclarator(j.objectPattern(propsToKeep), j.identifier(destructPropsIdentifierName)),
324
+ ]);
325
+ }
326
+ if (hasOtherProperties) {
327
+ /**
328
+ * If there are other properties, we need to keep the original param with destructuring
329
+ * e.g.
330
+ * input:
331
+ * Page({ params: { slug }, otherProp }) {
332
+ * const { slug } = await props.params;
333
+ * }
334
+ *
335
+ * output:
336
+ * Page(props) {
337
+ * const { otherProp } = props; // inserted
338
+ * // ...rest of the function body
339
+ * }
340
+ */
341
+ const restProperties = allProperties.filter((prop) => {
342
+ const isTargetProps = 'key' in prop &&
343
+ prop.key.type === 'Identifier' &&
344
+ utils_1.TARGET_PROP_NAMES.has(prop.key.name);
345
+ return !isTargetProps;
346
+ });
347
+ const destructionOtherPropertiesDeclaration = createDestructuringDeclaration(restProperties, propsIdentifierName);
348
+ if (functionBody && destructionOtherPropertiesDeclaration) {
349
+ functionBody.unshift(destructionOtherPropertiesDeclaration);
350
+ }
351
+ }
352
+ for (const [matchedPropName, paramsProperty] of propertiesMap) {
353
+ if (!utils_1.TARGET_PROP_NAMES.has(matchedPropName)) {
354
+ continue;
355
+ }
356
+ const propRenamedId = j.Identifier.check(paramsProperty)
357
+ ? paramsProperty.name
358
+ : null;
359
+ const propName = propRenamedId || matchedPropName;
360
+ // if propName is not used in lower scope, and it stars with unused prefix `_`,
361
+ // also skip the transformation
362
+ const hasDeclared = path.scope.declares(propName);
363
+ if (!hasDeclared && propName.startsWith('_'))
364
+ continue;
365
+ const propNameIdentifier = j.identifier(matchedPropName);
179
366
  const propsIdentifier = j.identifier(propsIdentifierName);
180
367
  const accessedPropId = j.memberExpression(propsIdentifier, propNameIdentifier);
181
368
  // Check param property value, if it's destructed, we need to destruct it as well
@@ -212,14 +399,14 @@ function transformDynamicProps(source, api, _filePath) {
212
399
  }
213
400
  }
214
401
  else {
215
- const isFromExport = path.value.type === 'ExportNamedDeclaration';
216
- if (isFromExport) {
402
+ // const isFromExport = true
403
+ if (!isClientComponent) {
217
404
  // If it's export function, populate the function to async
218
- if ((0, utils_1.isFunctionType)(path.value.declaration.type) &&
405
+ if ((0, utils_1.isFunctionType)(node.type) &&
219
406
  // Make TS happy
220
- 'async' in path.value.declaration) {
221
- path.value.declaration.async = true;
222
- (0, utils_1.turnFunctionReturnTypeToAsync)(path.value.declaration, j);
407
+ 'async' in node) {
408
+ node.async = true;
409
+ (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
223
410
  // Insert `const <propName> = await props.<propName>;` at the beginning of the function body
224
411
  const paramAssignment = j.variableDeclaration('const', [
225
412
  j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropId)),
@@ -243,42 +430,23 @@ function transformDynamicProps(source, api, _filePath) {
243
430
  }
244
431
  }
245
432
  }
246
- // Process Function Declarations
247
- // Matching: default export function XXX(...) { ... }
248
- const defaultExportFunctionDeclarations = root.find(j.ExportDefaultDeclaration, {
249
- declaration: {
250
- type: (type) => type === 'FunctionDeclaration' ||
251
- type === 'FunctionExpression' ||
252
- type === 'ArrowFunctionExpression',
253
- },
254
- });
255
- defaultExportFunctionDeclarations.forEach((path) => {
256
- renameAsyncPropIfExisted(path);
433
+ const defaultExportsDeclarations = root.find(j.ExportDefaultDeclaration);
434
+ defaultExportsDeclarations.forEach((path) => {
435
+ const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, () => true);
436
+ if (functionPath) {
437
+ renameAsyncPropIfExisted(functionPath, true);
438
+ }
257
439
  });
258
440
  // Matching Next.js functional named export of route entry:
259
441
  // - export function <named>(...) { ... }
260
442
  // - export const <named> = ...
261
- const targetNamedExportDeclarations = root.find(j.ExportNamedDeclaration,
262
- // Filter the name is in TARGET_NAMED_EXPORTS
263
- {
264
- declaration: {
265
- id: {
266
- name: (idName) => {
267
- return utils_1.TARGET_NAMED_EXPORTS.has(idName);
268
- },
269
- },
270
- },
271
- });
272
- targetNamedExportDeclarations.forEach((path) => {
273
- renameAsyncPropIfExisted(path);
443
+ const namedExportDeclarations = root.find(j.ExportNamedDeclaration);
444
+ namedExportDeclarations.forEach((path) => {
445
+ const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, (idName) => utils_1.TARGET_NAMED_EXPORTS.has(idName));
446
+ if (functionPath) {
447
+ renameAsyncPropIfExisted(functionPath, false);
448
+ }
274
449
  });
275
- // TDOO: handle targetNamedDeclarators
276
- // const targetNamedDeclarators = root.find(
277
- // j.VariableDeclarator,
278
- // (node) =>
279
- // node.id.type === 'Identifier' &&
280
- // TARGET_NAMED_EXPORTS.has(node.id.name)
281
- // )
282
450
  }
283
451
  const isClientComponent = (0, utils_1.determineClientDirective)(root, j, source);
284
452
  // Apply to `params` and `searchParams`
@@ -8,6 +8,11 @@ exports.isPromiseType = isPromiseType;
8
8
  exports.turnFunctionReturnTypeToAsync = turnFunctionReturnTypeToAsync;
9
9
  exports.insertReactUseImport = insertReactUseImport;
10
10
  exports.generateUniqueIdentifier = generateUniqueIdentifier;
11
+ exports.isFunctionScope = isFunctionScope;
12
+ exports.findClosetParentFunctionScope = findClosetParentFunctionScope;
13
+ exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
14
+ exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
15
+ exports.insertCommentOnce = insertCommentOnce;
11
16
  exports.TARGET_NAMED_EXPORTS = new Set([
12
17
  // For custom route
13
18
  'GET',
@@ -195,4 +200,151 @@ function generateUniqueIdentifier(defaultIdName, path, j) {
195
200
  const propsIdentifier = j.identifier(idName);
196
201
  return propsIdentifier;
197
202
  }
203
+ function isFunctionScope(path, j) {
204
+ if (!path)
205
+ return false;
206
+ const node = path.node;
207
+ // Check if the node is a function (declaration, expression, or arrow function)
208
+ return (j.FunctionDeclaration.check(node) ||
209
+ j.FunctionExpression.check(node) ||
210
+ j.ArrowFunctionExpression.check(node));
211
+ }
212
+ function findClosetParentFunctionScope(path, j) {
213
+ let parentFunctionPath = path.scope.path;
214
+ while (parentFunctionPath && !isFunctionScope(parentFunctionPath, j)) {
215
+ parentFunctionPath = parentFunctionPath.parent;
216
+ }
217
+ return parentFunctionPath;
218
+ }
219
+ function getFunctionNodeFromBinding(bindingPath, idName, j, root) {
220
+ const bindingNode = bindingPath.node;
221
+ if (j.FunctionDeclaration.check(bindingNode) ||
222
+ j.FunctionExpression.check(bindingNode) ||
223
+ j.ArrowFunctionExpression.check(bindingNode)) {
224
+ return bindingPath;
225
+ }
226
+ else if (j.VariableDeclarator.check(bindingNode)) {
227
+ const init = bindingNode.init;
228
+ // If the initializer is a function (arrow or function expression), record it
229
+ if (j.FunctionExpression.check(init) ||
230
+ j.ArrowFunctionExpression.check(init)) {
231
+ return bindingPath.get('init');
232
+ }
233
+ }
234
+ else if (j.Identifier.check(bindingNode)) {
235
+ const variablePath = root.find(j.VariableDeclaration, {
236
+ // declarations, each is VariableDeclarator
237
+ declarations: [
238
+ {
239
+ // VariableDeclarator
240
+ type: 'VariableDeclarator',
241
+ // id is Identifier
242
+ id: {
243
+ type: 'Identifier',
244
+ name: idName,
245
+ },
246
+ },
247
+ ],
248
+ });
249
+ if (variablePath.size()) {
250
+ const variableDeclarator = variablePath.get()?.node?.declarations?.[0];
251
+ if (j.VariableDeclarator.check(variableDeclarator)) {
252
+ const init = variableDeclarator.init;
253
+ if (j.FunctionExpression.check(init) ||
254
+ j.ArrowFunctionExpression.check(init)) {
255
+ return variablePath.get('declarations', 0, 'init');
256
+ }
257
+ }
258
+ }
259
+ const functionDeclarations = root.find(j.FunctionDeclaration, {
260
+ id: {
261
+ name: idName,
262
+ },
263
+ });
264
+ if (functionDeclarations.size()) {
265
+ return functionDeclarations.get();
266
+ }
267
+ }
268
+ return undefined;
269
+ }
270
+ function getFunctionPathFromExportPath(exportPath, j, root, namedExportFilter) {
271
+ // Default export
272
+ if (j.ExportDefaultDeclaration.check(exportPath.node)) {
273
+ const { declaration } = exportPath.node;
274
+ if (declaration) {
275
+ if (j.FunctionDeclaration.check(declaration) ||
276
+ j.FunctionExpression.check(declaration) ||
277
+ j.ArrowFunctionExpression.check(declaration)) {
278
+ return exportPath.get('declaration');
279
+ }
280
+ else if (j.Identifier.check(declaration)) {
281
+ const idName = declaration.name;
282
+ if (!namedExportFilter(idName))
283
+ return;
284
+ const exportBinding = exportPath.scope.getBindings()[idName]?.[0];
285
+ if (exportBinding) {
286
+ return getFunctionNodeFromBinding(exportBinding, idName, j, root);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ else if (
292
+ // Named exports
293
+ j.ExportNamedDeclaration.check(exportPath.node)) {
294
+ const namedExportPath = exportPath;
295
+ // extract the named exports, name specifiers, and default specifiers
296
+ const { declaration, specifiers } = namedExportPath.node;
297
+ if (declaration) {
298
+ if (j.VariableDeclaration.check(declaration)) {
299
+ const { declarations } = declaration;
300
+ for (const decl of declarations) {
301
+ if (j.VariableDeclarator.check(decl) && j.Identifier.check(decl.id)) {
302
+ const idName = decl.id.name;
303
+ if (!namedExportFilter(idName))
304
+ return;
305
+ // get bindings for each variable declarator
306
+ const exportBinding = namedExportPath.scope.getBindings()[idName]?.[0];
307
+ if (exportBinding) {
308
+ return getFunctionNodeFromBinding(exportBinding, idName, j, root);
309
+ }
310
+ }
311
+ }
312
+ }
313
+ else if (j.FunctionDeclaration.check(declaration) ||
314
+ j.FunctionExpression.check(declaration) ||
315
+ j.ArrowFunctionExpression.check(declaration)) {
316
+ const funcName = declaration.id?.name;
317
+ if (!namedExportFilter(funcName))
318
+ return;
319
+ return namedExportPath.get('declaration');
320
+ }
321
+ }
322
+ if (specifiers) {
323
+ for (const specifier of specifiers) {
324
+ if (j.ExportSpecifier.check(specifier)) {
325
+ const idName = specifier.local.name;
326
+ if (!namedExportFilter(idName))
327
+ return;
328
+ const exportBinding = namedExportPath.scope.getBindings()[idName]?.[0];
329
+ if (exportBinding) {
330
+ return getFunctionNodeFromBinding(exportBinding, idName, j, root);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ return undefined;
337
+ }
338
+ function wrapParentheseIfNeeded(hasChainAccess, j, expression) {
339
+ return hasChainAccess ? j.parenthesizedExpression(expression) : expression;
340
+ }
341
+ function insertCommentOnce(path, j, comment) {
342
+ if (path.node.comments) {
343
+ const hasComment = path.node.comments.some((commentNode) => commentNode.value === comment);
344
+ if (hasComment) {
345
+ return;
346
+ }
347
+ }
348
+ path.node.comments = [j.commentBlock(comment), ...(path.node.comments || [])];
349
+ }
198
350
  //# sourceMappingURL=utils.js.map
@@ -13,7 +13,7 @@ function transformer(file, api) {
13
13
  const importDecl = dynamicImportDeclaration.get(0).node;
14
14
  const dynamicImportName = importDecl.specifiers?.[0]?.local?.name;
15
15
  if (!dynamicImportName) {
16
- return root.toSource();
16
+ return file.source;
17
17
  }
18
18
  // Find call expressions where the callee is the imported 'dynamic'
19
19
  root