From 16b10753598496b8a639279b314ea3df731243be Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 9 Feb 2023 01:22:39 +0100 Subject: [PATCH] type check generateMetadata --- packages/next/src/server/typescript/index.ts | 13 ++ .../src/server/typescript/rules/metadata.ts | 170 +++++++++++------- 2 files changed, 120 insertions(+), 63 deletions(-) diff --git a/packages/next/src/server/typescript/index.ts b/packages/next/src/server/typescript/index.ts index c303ebb22a68e..43b537256a206 100644 --- a/packages/next/src/server/typescript/index.ts +++ b/packages/next/src/server/typescript/index.ts @@ -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( diff --git a/packages/next/src/server/typescript/rules/metadata.ts b/packages/next/src/server/typescript/rules/metadata.ts index fffdeafddb7f9..f7a01c8683c34 100644 --- a/packages/next/src/server/typescript/rules/metadata.ts +++ b/packages/next/src/server/typescript/rules/metadata.ts @@ -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' const TYPE_IMPORT = `import type { Metadata } from 'next'` // Find the `export const metadata = ...` node. @@ -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 } @@ -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, @@ -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 [] @@ -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, @@ -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 },