/
diagnosticFormatter.ts
230 lines (212 loc) · 7.58 KB
/
diagnosticFormatter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { codeFrameColumns } from 'next/dist/compiled/babel/code-frame'
import chalk from 'next/dist/compiled/chalk'
import path from 'path'
// eslint typescript has a bug with TS enums
/* eslint-disable no-shadow */
export enum DiagnosticCategory {
Warning = 0,
Error = 1,
Suggestion = 2,
Message = 3,
}
function getFormattedLayoutAndPageDiagnosticMessageText(
baseDir: string,
diagnostic: import('typescript').Diagnostic
) {
const message = diagnostic.messageText
const sourceFilepath =
diagnostic.file?.text.trim().match(/^\/\/ File: (.+)\n/)?.[1] || ''
if (sourceFilepath && typeof message !== 'string') {
const relativeSourceFile = path.relative(baseDir, sourceFilepath)
const type = /'typeof import\(".+page"\)'/.test(message.messageText)
? 'Page'
: 'Layout'
// Reference of error codes:
// https://github.com/Microsoft/TypeScript/blob/main/src/compiler/diagnosticMessages.json
switch (message.code) {
case 2344:
const filepathAndType = message.messageText.match(
/'typeof import\("(.+)"\)'.+'(.+)'/
)
if (filepathAndType) {
let main = `${type} "${chalk.bold(
relativeSourceFile
)}" does not match the required types of a Next.js ${type}.`
function processNext(
indent: number,
next?: import('typescript').DiagnosticMessageChain[]
) {
if (!next) return
for (const item of next) {
switch (item.code) {
case 2200:
const mismatchedField =
item.messageText.match(/The types of '(.+)'/)
if (mismatchedField) {
main += '\n' + ' '.repeat(indent * 2)
main += `"${chalk.bold(
mismatchedField[1]
)}" has the wrong type:`
}
break
case 2322:
const types = item.messageText.match(
/Type '(.+)' is not assignable to type '(.+)'./
)
if (types) {
main += '\n' + ' '.repeat(indent * 2)
if (
types[2] === 'PageComponent' ||
types[2] === 'LayoutComponent'
) {
main += `The exported ${type} component isn't correctly typed.`
} else {
main += `Expected "${chalk.bold(
types[2].replace(
'"__invalid_negative_number__"',
'number (>= 0)'
)
)}", got "${chalk.bold(types[1])}".`
}
}
break
case 2326:
main += '\n' + ' '.repeat(indent * 2)
main += `Invalid configuration:`
break
case 2739:
const invalidProp = item.messageText.match(
/Type '(.+)' is missing the following properties from type '(.+)'/
)
if (invalidProp) {
if (
invalidProp[1] === 'LayoutProps' ||
invalidProp[1] === 'PageProps'
) {
main += '\n' + ' '.repeat(indent * 2)
main += `Prop "${invalidProp[2]}" is incompatible with the ${type}.`
}
}
break
case 2559:
const invalid = item.messageText.match(/Type '(.+)' has/)
if (invalid) {
main += '\n' + ' '.repeat(indent * 2)
main += `Type "${chalk.bold(invalid[1])}" isn't allowed.`
}
break
case 2741:
const incompatPageProp = item.messageText.match(
/Property '(.+)' is missing in type 'PageProps'/
)
if (incompatPageProp) {
main += '\n' + ' '.repeat(indent * 2)
main += `Prop "${chalk.bold(
incompatPageProp[1]
)}" will never be passed. Remove it from the component's props.`
} else {
const extraLayoutProp = item.messageText.match(
/Property '(.+)' is missing in type 'LayoutProps' but required in type '(.+)'/
)
if (extraLayoutProp) {
main += '\n' + ' '.repeat(indent * 2)
main += `Prop "${chalk.bold(
extraLayoutProp[1]
)}" is not valid for this Layout, remove it to fix.`
}
}
break
default:
}
processNext(indent + 1, item.next)
}
}
processNext(1, message.next)
return main
}
break
case 2345:
const filepathAndInvalidExport = message.messageText.match(
/'typeof import\("(.+)"\)'.+Impossible<"(.+)">/
)
if (filepathAndInvalidExport) {
const main = `${type} "${chalk.bold(
relativeSourceFile
)}" exports an invalid "${chalk.bold(
filepathAndInvalidExport[2]
)}" field. ${type} should only export a default React component and configuration options. Learn more: https://nextjs.org/docs/messages/invalid-segment-export`
return main
}
break
default:
}
}
}
export async function getFormattedDiagnostic(
ts: typeof import('typescript'),
baseDir: string,
diagnostic: import('typescript').Diagnostic,
isAppDirEnabled?: boolean
): Promise<string> {
// If the error comes from .next/types/, we handle it specially.
const isLayoutOrPageError =
isAppDirEnabled &&
diagnostic.file?.fileName.includes(path.join(baseDir, '.next', 'types'))
let message = ''
const layoutReason = isLayoutOrPageError
? getFormattedLayoutAndPageDiagnosticMessageText(baseDir, diagnostic)
: null
const reason =
layoutReason ||
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
const category = diagnostic.category
switch (category) {
// Warning
case DiagnosticCategory.Warning: {
message += chalk.yellow.bold('Type warning') + ': '
break
}
// Error
case DiagnosticCategory.Error: {
message += chalk.red.bold('Type error') + ': '
break
}
// 2 = Suggestion, 3 = Message
case DiagnosticCategory.Suggestion:
case DiagnosticCategory.Message:
default: {
message += chalk.cyan.bold(category === 2 ? 'Suggestion' : 'Info') + ': '
break
}
}
message += reason + '\n'
if (!isLayoutOrPageError && diagnostic.file) {
const pos = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!)
const line = pos.line + 1
const character = pos.character + 1
let fileName = path.posix.normalize(
path.relative(baseDir, diagnostic.file.fileName).replace(/\\/g, '/')
)
if (!fileName.startsWith('.')) {
fileName = './' + fileName
}
message =
chalk.cyan(fileName) +
':' +
chalk.yellow(line.toString()) +
':' +
chalk.yellow(character.toString()) +
'\n' +
message
message +=
'\n' +
codeFrameColumns(
diagnostic.file.getFullText(diagnostic.file.getSourceFile()),
{
start: { line: line, column: character },
},
{ forceColor: true }
)
}
return message
}