Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IntelliSense for generateMetadata #45723

Merged
merged 2 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/next/src/server/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,19 @@ export function createTSPlugin(modules: {
ts.isFunctionDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (isAppEntry) {
const metadataDiagnostics = isClientEntry
? metadata.getSemanticDiagnosticsForExportVariableStatementInClientEntry(
fileName,
node
)
: metadata.getSemanticDiagnosticsForExportVariableStatement(
fileName,
node
)
prior.push(...metadataDiagnostics)
}

// export function ...
if (isClientEntry) {
prior.push(
Expand Down
170 changes: 107 additions & 63 deletions packages/next/src/server/typescript/rules/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NEXT_TS_ERRORS } from '../constant'
import { getInfo, getSource, getTs, isPositionInsideNode } from '../utils'

const TYPE_ANOTATION = ': Metadata'
const TYPE_ANOTATION_ASYNC = ': Promise<Metadata> | Metadata'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this doesn't behave as expected

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I'm considering removing the IntelliSense here completely. Although the downside is that if you don't type it manually, we can't catch wrong return value types.

const TYPE_IMPORT = `import type { Metadata } from 'next'`

// Find the `export const metadata = ...` node.
Expand Down Expand Up @@ -136,26 +137,42 @@ function getProxiedLanguageService() {

function updateVirtualFileWithType(
fileName: string,
node: ts.VariableDeclaration
node: ts.VariableDeclaration | ts.FunctionDeclaration,
isGenerateMetadata?: boolean
) {
const source = getSource(fileName)
if (!source) return

// We annotate with the type in a vritual language service
const sourceText = source.getFullText()
const nodeEnd = node.name.getFullStart() + node.name.getFullWidth()
let nodeEnd: number
let annotation: string

const ts = getTs()
if (ts.isFunctionDeclaration(node)) {
if (isGenerateMetadata) {
nodeEnd = node.body!.getFullStart()
annotation = TYPE_ANOTATION_ASYNC
} else {
return
}
} else {
nodeEnd = node.name.getFullStart() + node.name.getFullWidth()
annotation = TYPE_ANOTATION
}

const newSource =
sourceText.slice(0, nodeEnd) +
TYPE_ANOTATION +
annotation +
sourceText.slice(nodeEnd) +
TYPE_IMPORT
const { languageServiceHost } = getProxiedLanguageService()
languageServiceHost.addFile(fileName, newSource)

return nodeEnd
return [nodeEnd, annotation.length]
}

function isTyped(node: ts.VariableDeclaration) {
function isTyped(node: ts.VariableDeclaration | ts.FunctionDeclaration) {
return node.type !== undefined
}

Expand All @@ -173,13 +190,12 @@ const metadata = {
const ts = getTs()

// We annotate with the type in a vritual language service
const nodeEnd = updateVirtualFileWithType(fileName, node)
if (nodeEnd === undefined) return prior
const pos = updateVirtualFileWithType(fileName, node)
if (pos === undefined) return prior

// Get completions
const { languageService } = getProxiedLanguageService()
const newPos =
position <= nodeEnd ? position : position + TYPE_ANOTATION.length
const newPos = position <= pos[0] ? position : position + pos[1]
const completions = languageService.getCompletionsAtPosition(
fileName,
newPos,
Expand Down Expand Up @@ -225,75 +241,105 @@ const metadata = {

getSemanticDiagnosticsForExportVariableStatementInClientEntry(
fileName: string,
node: ts.VariableStatement
node: ts.VariableStatement | ts.FunctionDeclaration
) {
const source = getSource(fileName)
const ts = getTs()

for (const declaration of node.declarationList.declarations) {
const name = declaration.name.getText()
if (['metadata', 'generateMetadata'].includes(name)) {
// It is not allowed to use `metadata` or `generateMetadata` in client entry
// It is not allowed to export `metadata` or `generateMetadata` in client entry
if (ts.isFunctionDeclaration(node)) {
if (node.name?.getText() === 'generateMetadata') {
return [
{
file: source,
category: ts.DiagnosticCategory.Error,
code: NEXT_TS_ERRORS.INVALID_METADATA_EXPORT,
messageText: `The Next.js '${name}' API is not allowed in a client component.`,
start: declaration.name.getStart(),
length: declaration.name.getWidth(),
messageText: `The Next.js 'generateMetadata' API is not allowed in a client component.`,
start: node.name.getStart(),
length: node.name.getWidth(),
},
]
}
} else {
for (const declaration of node.declarationList.declarations) {
const name = declaration.name.getText()
if (name === 'metadata') {
return [
{
file: source,
category: ts.DiagnosticCategory.Error,
code: NEXT_TS_ERRORS.INVALID_METADATA_EXPORT,
messageText: `The Next.js 'metadata' API is not allowed in a client component.`,
start: declaration.name.getStart(),
length: declaration.name.getWidth(),
},
]
}
}
}
return []
},

getSemanticDiagnosticsForExportVariableStatement(
fileName: string,
node: ts.VariableStatement
node: ts.VariableStatement | ts.FunctionDeclaration
) {
const source = getSource(fileName)
const ts = getTs()

for (const declaration of node.declarationList.declarations) {
if (declaration.name.getText() === 'metadata') {
if (isTyped(declaration)) break
function proxyDiagnostics(
pos: number[],
n: ts.VariableDeclaration | ts.FunctionDeclaration
) {
// Get diagnostics
const { languageService } = getProxiedLanguageService()
const diagnostics = languageService.getSemanticDiagnostics(fileName)

// Filter and map the results
return diagnostics
.filter((d) => {
if (d.start === undefined || d.length === undefined) return false
if (d.start < n.getFullStart()) return false
if (
d.start + d.length >=
n.getFullStart() + n.getFullWidth() + pos[1]
)
return false
return true
})
.map((d) => {
return {
file: source,
category: d.category,
code: d.code,
messageText: d.messageText,
start: d.start! < pos[0] ? d.start : d.start! - pos[1],
length: d.length,
}
})
}

if (ts.isFunctionDeclaration(node)) {
if (node.name?.getText() === 'generateMetadata') {
if (isTyped(node)) return []

// We annotate with the type in a vritual language service
const nodeEnd = updateVirtualFileWithType(fileName, declaration)
if (!nodeEnd) break

// Get diagnostics
const { languageService } = getProxiedLanguageService()
const diagnostics = languageService.getSemanticDiagnostics(fileName)

// Filter and map the results
return diagnostics
.filter((d) => {
if (d.start === undefined || d.length === undefined) return false
if (d.start < declaration.getFullStart()) return false
if (
d.start + d.length >
declaration.getFullStart() +
declaration.getFullWidth() +
TYPE_ANOTATION.length
)
return false
return true
})
.map((d) => {
return {
file: source,
category: d.category,
code: d.code,
messageText: d.messageText,
start:
d.start! <= nodeEnd
? d.start
: d.start! - TYPE_ANOTATION.length,
length: d.length,
}
})
const pos = updateVirtualFileWithType(fileName, node, true)
if (!pos) return []

return proxyDiagnostics(pos, node)
}
} else {
for (const declaration of node.declarationList.declarations) {
if (declaration.name.getText() === 'metadata') {
if (isTyped(declaration)) break

// We annotate with the type in a vritual language service
const pos = updateVirtualFileWithType(fileName, declaration)
if (!pos) break

return proxyDiagnostics(pos, declaration)
}
}
}
return []
Expand All @@ -313,12 +359,11 @@ const metadata = {
if (isTyped(node)) return

// We annotate with the type in a vritual language service
const nodeEnd = updateVirtualFileWithType(fileName, node)
if (nodeEnd === undefined) return
const pos = updateVirtualFileWithType(fileName, node)
if (pos === undefined) return

const { languageService } = getProxiedLanguageService()
const newPos =
position <= nodeEnd ? position : position + TYPE_ANOTATION.length
const newPos = position <= pos[0] ? position : position + pos[1]

const details = languageService.getCompletionEntryDetails(
fileName,
Expand All @@ -338,12 +383,11 @@ const metadata = {
if (isTyped(node)) return

// We annotate with the type in a vritual language service
const nodeEnd = updateVirtualFileWithType(fileName, node)
if (nodeEnd === undefined) return
const pos = updateVirtualFileWithType(fileName, node)
if (pos === undefined) return

const { languageService } = getProxiedLanguageService()
const newPos =
position <= nodeEnd ? position : position + TYPE_ANOTATION.length
const newPos = position <= pos[0] ? position : position + pos[1]
const insight = languageService.getQuickInfoAtPosition(fileName, newPos)
return insight
},
Expand Down