55 isPositionInsideNode ,
66 getTs ,
77 removeStringQuotes ,
8+ getTypeChecker ,
89} from '../utils'
910import { NEXT_TS_ERRORS , ALLOWED_EXPORTS } from '../constant'
1011import type tsModule from 'typescript/lib/tsserverlibrary'
@@ -308,9 +309,49 @@ const config = {
308309 API_DOCS [ entryConfig ] . link ,
309310 }
310311
311- if ( value && isPositionInsideNode ( position , value ) ) {
312- // Hovers the value of the config
313- const isString = ts . isStringLiteral ( value )
312+ // When the value is a flexible type (like a function), also compute its
313+ // inferred type so we can surface it alongside the docs. This is useful
314+ // even when the value is considered invalid by the config validation,
315+ // as long as it's not a direct literal export.
316+ let displayParts : tsModule . SymbolDisplayPart [ ] = [ ]
317+ const typeChecker = getTypeChecker ( )
318+ const isString = ! ! value && ts . isStringLiteral ( value )
319+ const isFunctionValue =
320+ ! ! value &&
321+ ! isString &&
322+ ( ts . isArrowFunction ( value ) ||
323+ ts . isFunctionExpression ( value ) ||
324+ ts . isFunctionDeclaration ( value ) )
325+
326+ if ( typeChecker && value && isFunctionValue ) {
327+ try {
328+ // If we're hovering the config identifier, ask for the type at the
329+ // identifier; otherwise, ask at the value node. This makes sure
330+ // highlighting `generateMetadata` itself also shows the inferred type.
331+ const typeTarget = isPositionInsideNode ( position , name ) ? name : value
332+ const type = typeChecker . getTypeAtLocation ( typeTarget )
333+ if ( type ) {
334+ const typeString = typeChecker . typeToString ( type , typeTarget )
335+ if ( typeString ) {
336+ displayParts = [
337+ {
338+ text : typeString ,
339+ kind : 'typeName' ,
340+ } ,
341+ ]
342+ }
343+ }
344+ } catch {
345+ // If type checking fails, continue without type info.
346+ }
347+ }
348+
349+ // For non-function values (like literals), hovering the value should show
350+ // option-specific docs. For function-valued configs (e.g. `generateMetadata`),
351+ // we let TypeScript handle hover anywhere in the initializer except for the
352+ // export identifier itself.
353+ if ( value && ! isFunctionValue && isPositionInsideNode ( position , value ) ) {
354+ // Hovering the value of the config
314355 const text = removeStringQuotes ( value . getText ( ) )
315356 const key = isString ? `"${ text } "` : text
316357
@@ -339,19 +380,32 @@ const config = {
339380 ] ,
340381 }
341382 } else {
342- // Wrong value, display the docs link
383+ // Wrong value: still show the docs link, and when available, the
384+ // inferred type for non-literal (i.e. non-direct) exports.
343385 overridden = {
344386 kind : ts . ScriptElementKind . enumElement ,
345387 kindModifiers : ts . ScriptElementKindModifier . none ,
346388 textSpan : {
347389 start : value . getStart ( ) ,
348390 length : value . getWidth ( ) ,
349391 } ,
350- displayParts : [ ] ,
392+ displayParts,
351393 documentation : [ docsLink ] ,
352394 }
353395 }
354396 } else {
397+ // For function-valued configs, if we're hovering anywhere within the
398+ // initializer (including `async`, parameters, or the body) but not on
399+ // the export identifier itself, don't override TypeScript's default
400+ // hover. We only want to override when hovering the config identifier
401+ // (e.g. `generateMetadata`), not arbitrary tokens within the function.
402+ if (
403+ isFunctionValue &&
404+ isPositionInsideNode ( position , value ) && // hover is somewhere within the function initializer
405+ ! isPositionInsideNode ( position , name ) // ...but not on the export identifier itself
406+ ) {
407+ return
408+ }
355409 // Hovers the name of the config
356410 overridden = {
357411 kind : ts . ScriptElementKind . enumElement ,
@@ -360,7 +414,7 @@ const config = {
360414 start : name . getStart ( ) ,
361415 length : name . getWidth ( ) ,
362416 } ,
363- displayParts : [ ] ,
417+ displayParts,
364418 documentation : [
365419 {
366420 kind : 'text' ,
0 commit comments