diff --git a/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.input.tsx b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.input.tsx new file mode 100644 index 00000000000000..f9af9ef8c9c5cc --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.input.tsx @@ -0,0 +1,2 @@ +export { Page as default } from './page' +export { generateMetadata } from './generate-metadata' diff --git a/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.output.tsx b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.output.tsx new file mode 100644 index 00000000000000..2ce6908dd6c8b5 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-28.output.tsx @@ -0,0 +1,5 @@ +export { /* Next.js Dynamic Async API Codemod: `Page` export is re-exported. Check if this component uses `params` or `searchParams`*/ +Page as default } from './page' +export { /* Next.js Dynamic Async API Codemod: `generateMetadata` export is re-exported. Check if this component uses `params` or `searchParams`*/ +generateMetadata } from './generate-metadata' + \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.input.tsx b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.input.tsx new file mode 100644 index 00000000000000..521f9b4536d624 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.input.tsx @@ -0,0 +1,3 @@ +import { generateMetadata } from './generate-metadata' +export { default } from './page' +export { generateMetadata } diff --git a/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.output.tsx b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.output.tsx new file mode 100644 index 00000000000000..776e08f870e5b9 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-async-request-api-dynamic-props/access-props-29.output.tsx @@ -0,0 +1,5 @@ +import { generateMetadata } from './generate-metadata' +export { /* Next.js Dynamic Async API Codemod: `default` export is re-exported. Check if this component uses `params` or `searchParams`*/ +default } from './page' +export { /* Next.js Dynamic Async API Codemod: `generateMetadata` export is re-exported. Check if this component uses `params` or `searchParams`*/ +generateMetadata } diff --git a/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-api.ts b/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-api.ts index 7ecc81aa56241f..b9ee332eb2f8fd 100644 --- a/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-api.ts +++ b/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-api.ts @@ -29,8 +29,12 @@ function findDynamicImportsAndComment(root: Collection, j: API['j']) { }) importPaths.forEach((path) => { - insertCommentOnce(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT) - modified = true + const inserted = insertCommentOnce( + path.node, + j, + DYNAMIC_IMPORT_WARN_COMMENT + ) + modified ||= inserted }) return modified } @@ -180,7 +184,7 @@ export function transformDynamicAPI( ) needsReactUseImport = true } else { - castTypesOrAddComment( + const casted = castTypesOrAddComment( j, path, originRequestApiName, @@ -189,9 +193,10 @@ export function transformDynamicAPI( insertedTypes, ` Next.js Dynamic Async API Codemod: Manually await this call, if it's a Server Component ` ) + modified ||= casted } } else { - castTypesOrAddComment( + const casted = castTypesOrAddComment( j, path, originRequestApiName, @@ -200,8 +205,8 @@ export function transformDynamicAPI( insertedTypes, ' Next.js Dynamic Async API Codemod: please manually await this call, codemod cannot transform due to undetermined async scope ' ) + modified ||= casted } - modified = true } } }) @@ -263,9 +268,8 @@ export function transformDynamicAPI( insertReactUseImport(root, j) } - if (findDynamicImportsAndComment(root, j)) { - modified = true - } + const commented = findDynamicImportsAndComment(root, j) + modified ||= commented return modified ? root.toSource() : null } @@ -286,10 +290,11 @@ function castTypesOrAddComment( insertedTypes: Set, customMessage: string ) { + let modified = false const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx') if (isTsFile) { // if the path of call expression is already being awaited, no need to cast - if (path.parentPath?.node?.type === 'AwaitExpression') return + if (path.parentPath?.node?.type === 'AwaitExpression') return false /* Do type cast for headers, cookies, draftMode import { @@ -314,6 +319,7 @@ function castTypesOrAddComment( // Replace the original expression with the new cast expression, // also wrap () around the new cast expression. j(path).replaceWith(j.parenthesizedExpression(newCastExpression)) + modified = true // If cast types are not imported, add them to the import list const importDeclaration = root.find(j.ImportDeclaration, { @@ -343,8 +349,11 @@ function castTypesOrAddComment( } } else { // Otherwise for JS file, leave a message to the user to manually handle the transformation - insertCommentOnce(path.node, j, customMessage) + const inserted = insertCommentOnce(path.node, j, customMessage) + modified ||= inserted } + + return modified } function findImportMappingFromNextHeaders(root: Collection, j: API['j']) { diff --git a/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-prop.ts b/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-prop.ts index 98a68e0cb4548d..f722342cc95141 100644 --- a/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-prop.ts +++ b/packages/next-codemod/transforms/lib/async-request-api/next-async-dynamic-prop.ts @@ -22,6 +22,7 @@ import { } from './utils' const PAGE_PROPS = 'props' +const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout|route)\.(t|j)sx?$/ function findFunctionBody(path: ASTPath) { let functionBody = path.node.body @@ -175,14 +176,64 @@ function applyUseAndRenameAccessedProp( return modified } -const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout|route)\.(t|j)sx?$/ +function commentOnMatchedReExports( + root: Collection, + j: API['jscodeshift'] +): boolean { + let modified = false + root.find(j.ExportNamedDeclaration).forEach((path) => { + if (j.ExportSpecifier.check(path.value.specifiers[0])) { + const specifiers = path.value.specifiers + for (const specifier of specifiers) { + if ( + j.ExportSpecifier.check(specifier) && + // Find matched named exports and default export + (TARGET_NAMED_EXPORTS.has(specifier.exported.name) || + specifier.exported.name === 'default') + ) { + if (j.Literal.check(path.value.source)) { + const localName = specifier.local.name + + const commentInserted = insertCommentOnce( + specifier, + j, + ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\`` + ) + modified ||= commentInserted + } else if (path.value.source === null) { + const localIdentifier = specifier.local + const localName = localIdentifier.name + // search if local identifier is from imports + const importDeclaration = root + .find(j.ImportDeclaration) + .filter((importPath) => { + return importPath.value.specifiers.some( + (importSpecifier) => importSpecifier.local.name === localName + ) + }) + if (importDeclaration.size() > 0) { + const commentInserted = insertCommentOnce( + specifier, + j, + ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\`` + ) + modified ||= commentInserted + } + } + } + } + } + }) + return modified +} function modifyTypes( paramTypeAnnotation: any, propsIdentifier: Identifier, root: Collection, j: API['jscodeshift'] -) { +): boolean { + let modified = false if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) { const typeAnnotation = paramTypeAnnotation.typeAnnotation if (typeAnnotation.type === 'TSTypeLiteral') { @@ -220,6 +271,7 @@ function modifyTypes( member.typeAnnotation.typeAnnotation, ]) ) + modified = true } } }) @@ -270,6 +322,7 @@ function modifyTypes( member.typeAnnotation.typeAnnotation, ]) ) + modified = true } } }) @@ -279,7 +332,9 @@ function modifyTypes( } propsIdentifier.typeAnnotation = paramTypeAnnotation + modified = true } + return modified } export function transformDynamicProps( @@ -380,7 +435,8 @@ export function transformDynamicProps( modified = true } } else { - modified = awaitMemberAccessOfProp(argName, path, j) + const awaited = awaitMemberAccessOfProp(argName, path, j) + modified ||= awaited } // cases of passing down `props` into any function @@ -407,9 +463,8 @@ export function transformDynamicProps( (arg) => j.Identifier.check(arg) && arg.name === argName ) const comment = ` Next.js Dynamic Async API Codemod: '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. ` - insertCommentOnce(propPassedAsArg, j, comment) - - modified = true + const inserted = insertCommentOnce(propPassedAsArg, j, comment) + modified ||= inserted }) if (modified) { @@ -438,9 +493,14 @@ export function transformDynamicProps( } else { // When the prop argument is not destructured, we need to add comments to the spread properties if (j.Identifier.check(currentParam)) { - commentSpreadProps(path, currentParam.name, j) - modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j) - modified = true + const commented = commentSpreadProps(path, currentParam.name, j) + const modifiedTypes = modifyTypes( + currentParam.typeAnnotation, + propsIdentifier, + root, + j + ) + modified ||= commented || modifiedTypes } } } @@ -754,6 +814,9 @@ export function transformDynamicProps( insertReactUseImport(root, j) } + const commented = commentOnMatchedReExports(root, j) + modified ||= commented + return modified ? root.toSource() : null } @@ -824,7 +887,8 @@ function commentSpreadProps( path: ASTPath, propsIdentifierName: string, j: API['jscodeshift'] -) { +): boolean { + let modified = false const functionBody = findFunctionBody(path) const functionBodyCollection = j(functionBody) // Find all the usage of spreading properties of `props` @@ -839,10 +903,14 @@ function commentSpreadProps( // Add comment before it jsxSpreadProperties.forEach((spread) => { - insertCommentOnce(spread.value, j, comment) + const inserted = insertCommentOnce(spread.value, j, comment) + if (inserted) modified = true }) objSpreadProperties.forEach((spread) => { - insertCommentOnce(spread.value, j, comment) + const inserted = insertCommentOnce(spread.value, j, comment) + if (inserted) modified = true }) + + return modified } diff --git a/packages/next-codemod/transforms/lib/async-request-api/utils.ts b/packages/next-codemod/transforms/lib/async-request-api/utils.ts index 616ae29f6a53eb..6e48f4b4b750af 100644 --- a/packages/next-codemod/transforms/lib/async-request-api/utils.ts +++ b/packages/next-codemod/transforms/lib/async-request-api/utils.ts @@ -427,16 +427,17 @@ export function insertCommentOnce( node: ASTPath['node'], j: API['j'], comment: string -) { +): boolean { if (node.comments) { const hasComment = node.comments.some( (commentNode) => commentNode.value === comment ) if (hasComment) { - return + return false } } node.comments = [j.commentBlock(comment), ...(node.comments || [])] + return true } export function getVariableDeclaratorId(