Skip to content

Commit

Permalink
handle generateMetadata
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Sep 17, 2024
1 parent 2df12b3 commit 9110080
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// If it's sync default export, convert to async and await the function call
import { draftMode } from 'next/headers'

export default async function MyComponent() {
(await draftMode()).enable()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { headers } from 'next/headers'

export function generateMetadata() {
headers()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { headers } from 'next/headers'

export async function generateMetadata() {
await headers()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { headers } from 'next/headers'

export const generateMetadata = function () {
headers()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { headers } from 'next/headers'

export const generateMetadata = async function() {
await headers()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { use } from "react";
import { cookies } from "next/headers";

function MyComponent() {
callSomething(use(cookies()));
callSomething(cookies());
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { API, ASTPath, CallExpression, Collection } from 'jscodeshift'
import type {
API,
ASTPath,
CallExpression,
Collection,
FunctionDeclaration,
} from 'jscodeshift'

type AsyncAPIName = 'cookies' | 'headers' | 'draftMode'

function isFunctionType(type: string) {
return (
type === 'FunctionDeclaration' ||
type === 'FunctionExpression' ||
type === 'ArrowFunctionExpression'
)
}

function insertReactUseImport(root: Collection<any>, j: API['j']) {
const hasReactUseImport =
root
Expand Down Expand Up @@ -53,18 +67,91 @@ function insertReactUseImport(root: Collection<any>, j: API['j']) {
}
}

function isSameNode(childNode, parentNode, j: API['j']) {
// Start from the child node and move up the AST
function isMatchedFunctionExported(
path: ASTPath<FunctionDeclaration>,
j: API['jscodeshift']
): boolean {
const GENERATE_METADATA_FUNCTION_NAME = 'generateMetadata'
// Check for direct export (`export function generateMetadata() {}`)
const directMetadataAPIExport = j(path).closest(j.ExportNamedDeclaration, {
declaration: {
type: 'FunctionDeclaration',
id: {
name: GENERATE_METADATA_FUNCTION_NAME,
},
},
})

if (!childNode || !parentNode) {
return false
if (directMetadataAPIExport.size() > 0) {
return true
}

if (j(childNode).toSource() === j(parentNode).toSource()) {
// Check for default export (`export default function() {}`)
const isDefaultExport = j(path).closest(j.ExportDefaultDeclaration).size() > 0
if (isDefaultExport) {
return true
}

return false
// Look for named export elsewhere in the file (`export { generateMetadata }`)
const root = j(path).closestScope().closest(j.Program)
const isNamedExport =
root
.find(j.ExportNamedDeclaration, {
specifiers: [
{
type: 'ExportSpecifier',
exported: {
name: GENERATE_METADATA_FUNCTION_NAME,
},
},
],
})
.size() > 0

// Look for variable export but still function, e.g. `export const generateMetadata = function() {}`,
// also check if variable is a function or arrow function
const isVariableExport =
root
.find(j.ExportNamedDeclaration, {
declaration: {
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: GENERATE_METADATA_FUNCTION_NAME,
},
init: {
type: isFunctionType,
},
},
],
},
})
.size() > 0

if (isVariableExport) return true

return isNamedExport
}

function findImportedIdentifier(
root: Collection<any>,
j: API['j'],
functionName: AsyncAPIName
) {
let importedAlias: string | undefined
root
.find(j.ImportDeclaration, {
source: { value: 'next/headers' },
})
.find(j.ImportSpecifier, {
imported: { name: functionName },
})
.forEach((importSpecifier) => {
importedAlias = importSpecifier.node.local.name
})
return importedAlias
}

export function transformDynamicAPI(source: string, api: API) {
Expand All @@ -74,21 +161,6 @@ export function transformDynamicAPI(source: string, api: API) {
// Check if 'use' from 'react' needs to be imported
let needsReactUseImport = false

function findImportedIdentifier(functionName: AsyncAPIName) {
let importedAlias: string | undefined
root
.find(j.ImportDeclaration, {
source: { value: 'next/headers' },
})
.find(j.ImportSpecifier, {
imported: { name: functionName },
})
.forEach((importSpecifier) => {
importedAlias = importSpecifier.node.local.name
})
return importedAlias
}

function isImportedInModule(
path: ASTPath<CallExpression>,
functionName: string
Expand All @@ -100,22 +172,13 @@ export function transformDynamicAPI(source: string, api: API) {
}

function processAsyncApiCalls(functionName: AsyncAPIName) {
const importedAlias = findImportedIdentifier(functionName)
const importedAlias = findImportedIdentifier(root, j, functionName)

if (!importedAlias) {
// Skip the transformation if the function is not imported from 'next/headers'
return
}

const defaultExportFunctionPath = root.find(j.ExportDefaultDeclaration, {
declaration: {
type: (type) =>
type === 'FunctionDeclaration' ||
type === 'FunctionExpression' ||
type === 'ArrowFunctionExpression',
},
})

// Process each call to cookies() or headers()
root
.find(j.CallExpression, {
Expand All @@ -130,8 +193,15 @@ export function transformDynamicAPI(source: string, api: API) {
return
}

const closetScope = j(path).closestScope()

// Check if available to apply transform
const closestFunction = j(path).closest(j.FunctionDeclaration)
const closestFunction =
j(path).closest(j.FunctionDeclaration) ||
j(path).closest(j.FunctionExpression) ||
j(path).closest(j.ArrowFunctionExpression) ||
j(path).closest(j.VariableDeclaration)

const isAsyncFunction = closestFunction
.nodes()
.some((node) => node.async)
Expand All @@ -149,35 +219,42 @@ export function transformDynamicAPI(source: string, api: API) {
)
}
} else {
// Check if current path is under the defaultExportFunction, without using any helper
const defaultExportFunctionNode = defaultExportFunctionPath.size()
? defaultExportFunctionPath.get().node
// Determine if the function is an export
const isFromExport = isMatchedFunctionExported(closetScope.get(), j)
const closestFunctionNode = closetScope.size()
? closetScope.get().node
: null

const closestFunctionNode = closestFunction.get().node

// Determine if defaultExportFunctionNode contains the current path
const isUnderDefaultExportFunction = defaultExportFunctionNode
? isSameNode(
closestFunctionNode,
defaultExportFunctionNode.declaration,
j
)
: false
// If it's exporting a function directly, exportFunctionNode is same as exportNode
// e.g. export default function MyComponent() {}
// If it's exporting a variable declaration, exportFunctionNode is the function declaration
// e.g. export const MyComponent = function() {}
let exportFunctionNode

if (isFromExport) {
if (
closestFunctionNode &&
isFunctionType(closestFunctionNode.type)
) {
exportFunctionNode = closestFunctionNode
}
} else {
// Is normal async function
exportFunctionNode = closestFunctionNode
}

let canConvertToAsync = false
// check if current path is under the default export function
if (isUnderDefaultExportFunction) {
if (isFromExport) {
// if default export function is not async, convert it to async, and await the api call

if (!isCallAwaited) {
if (defaultExportFunctionNode.declaration) {
defaultExportFunctionNode.declaration.async = true
canConvertToAsync = true
}
if (defaultExportFunctionNode.expression) {
defaultExportFunctionNode.expression.async = true
// If the scoped function is async function
if (
isFunctionType(exportFunctionNode.type) &&
exportFunctionNode.async === false
) {
canConvertToAsync = true
exportFunctionNode.async = true
}

if (canConvertToAsync) {
Expand All @@ -190,17 +267,22 @@ export function transformDynamicAPI(source: string, api: API) {
}
} else {
// if parent is function function and it's a hook, which starts with 'use', wrap the api call with 'use()'
const parentFunction = j(path).closest(j.FunctionDeclaration)
const isParentFunctionHook =
parentFunction.size() > 0 &&
parentFunction.get().node.id?.name.startsWith('use')
if (isParentFunctionHook) {
j(path).replaceWith(
j.callExpression(j.identifier('use'), [
j.callExpression(j.identifier(functionName), []),
])
)
needsReactUseImport = true
const parentFunction =
j(path).closest(j.FunctionDeclaration) ||
j(path).closest(j.FunctionExpression) ||
j(path).closest(j.ArrowFunctionExpression)

if (parentFunction.size() > 0) {
const parentFUnctionName = parentFunction.get().node.id?.name
const isParentFunctionHook = parentFUnctionName?.startsWith('use')
if (isParentFunctionHook) {
j(path).replaceWith(
j.callExpression(j.identifier('use'), [
j.callExpression(j.identifier(functionName), []),
])
)
needsReactUseImport = true
}
} else {
// TODO: Otherwise, leave a message to the user to manually handle the transformation
}
Expand Down
Loading

0 comments on commit 9110080

Please sign in to comment.