Skip to content

Commit 071d839

Browse files
agadzikdevjiwonchoi
authored andcommitted
Attempts to fix the tssserver performance issues
1 parent a43e571 commit 071d839

File tree

3 files changed

+170
-136
lines changed

3 files changed

+170
-136
lines changed

packages/next/src/server/typescript/index.ts

+95-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isPositionInsideNode,
1717
getSource,
1818
isInsideApp,
19+
type PluginCreateInfo,
1920
} from './utils'
2021
import { NEXT_TS_ERRORS } from './constant'
2122

@@ -32,11 +33,103 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
3233
typescript: ts,
3334
}) => {
3435
function create(info: tsModule.server.PluginCreateInfo) {
36+
const logger = info.project.projectService.logger
37+
38+
logger.info('[Next.js] Initializing...')
39+
3540
init({
3641
ts,
37-
info,
42+
info: info as PluginCreateInfo,
3843
})
3944

45+
logger.info('[Next.js] Initialized!')
46+
47+
const virtualFiles: Record<
48+
string,
49+
{ file: tsModule.IScriptSnapshot; ver: number }
50+
> = {}
51+
const getScriptVersion = info.languageServiceHost.getScriptVersion.bind(
52+
info.languageServiceHost
53+
)
54+
const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(
55+
info.languageServiceHost
56+
)
57+
const getScriptFileNames = info.languageServiceHost.getScriptFileNames.bind(
58+
info.languageServiceHost
59+
)
60+
const readFile = info.languageServiceHost.readFile.bind(
61+
info.languageServiceHost
62+
)
63+
const fileExists = info.languageServiceHost.fileExists.bind(
64+
info.languageServiceHost
65+
)
66+
67+
info.languageServiceHost.getScriptVersion = (fileName: string) => {
68+
logger.info(`[ProxiedLSHost] getScriptVersion(${fileName})`)
69+
const file = virtualFiles[fileName]
70+
if (!file) return getScriptVersion(fileName)
71+
logger.info(`[ProxiedLSHost] getScriptVersion(${fileName}) - ${file.ver}`)
72+
return file.ver.toString()
73+
}
74+
75+
info.languageServiceHost.getScriptSnapshot = (fileName: string) => {
76+
logger.info(`[ProxiedLSHost] getScriptSnapshot(${fileName})`)
77+
const file = virtualFiles[fileName]
78+
if (!file) return getScriptSnapshot(fileName)
79+
logger.info(
80+
`[ProxiedLSHost] getScriptSnapshot(${fileName}) - ${JSON.stringify(file.file, null, 2)}`
81+
)
82+
return file.file
83+
}
84+
85+
info.languageServiceHost.getScriptFileNames = () => {
86+
const names: Set<string> = new Set()
87+
for (var name in virtualFiles) {
88+
if (virtualFiles.hasOwnProperty(name)) {
89+
names.add(name)
90+
}
91+
}
92+
const files = getScriptFileNames()
93+
for (const file of files) {
94+
names.add(file)
95+
}
96+
logger.info(
97+
`[ProxiedLSHost] getScriptFileNames() - ${JSON.stringify([...names], null, 2)}`
98+
)
99+
return [...names]
100+
}
101+
102+
info.languageServiceHost.readFile = (fileName: string) => {
103+
logger.info(`[ProxiedLSHost] readFile(${fileName})`)
104+
const file = virtualFiles[fileName]
105+
return file
106+
? file.file.getText(0, file.file.getLength())
107+
: readFile(fileName)
108+
}
109+
110+
info.languageServiceHost.fileExists = (fileName: string) => {
111+
logger.info(`[ProxiedLSHost] fileExists(${fileName})`)
112+
return !!virtualFiles[fileName] || fileExists(fileName)
113+
}
114+
115+
// @ts-ignore
116+
info.languageServiceHost.addFile = (fileName: string, body: string) => {
117+
logger.info(`[ProxiedLSHost] addFile(${fileName})\n\n${body}\n<<EOF>>`)
118+
const snap = ts.ScriptSnapshot.fromString(body)
119+
snap.getChangeRange = (_) => undefined
120+
const existing = virtualFiles[fileName]
121+
if (existing) {
122+
virtualFiles[fileName].ver++
123+
virtualFiles[fileName].file = snap
124+
} else {
125+
virtualFiles[fileName] = { ver: 2, file: snap }
126+
}
127+
128+
// This is the same function call that the Svelte TS plugin makes
129+
// @ts-expect-error internal API since TS 5.5
130+
info.project.markAsDirty?.()
131+
}
132+
40133
// Set up decorator object
41134
const proxy = Object.create(null)
42135
for (let k of Object.keys(info.languageService)) {
@@ -71,6 +164,7 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
71164
isNewIdentifierLocation: false,
72165
entries: [],
73166
}
167+
74168
if (!isAppEntryFile(fileName)) return prior
75169

76170
// If it's a server entry.

packages/next/src/server/typescript/rules/metadata.ts

+65-129
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@ import {
66
getTypeChecker,
77
isPositionInsideNode,
88
} from '../utils'
9-
109
import type tsModule from 'typescript/lib/tsserverlibrary'
1110

1211
const TYPE_ANNOTATION = ': Metadata | null'
1312
const TYPE_ANNOTATION_ASYNC = ': Promise<Metadata | null>'
1413
const TYPE_IMPORT = `\n\nimport type { Metadata } from 'next'`
1514

15+
const updatedFilePositionsCache = new Map<string, number[]>()
16+
17+
function cacheKey(
18+
fileName: string,
19+
isFunction: boolean,
20+
isGenerateMetadata?: boolean
21+
) {
22+
return `${fileName}:${isFunction ? 'function' : 'variable'}:${isGenerateMetadata ? 'generateMetadata' : 'metadata'}`
23+
}
24+
1625
// Find the `export const metadata = ...` node.
1726
function getMetadataExport(fileName: string, position: number) {
1827
const source = getSource(fileName)
@@ -49,139 +58,67 @@ function getMetadataExport(fileName: string, position: number) {
4958
return metadataExport
5059
}
5160

52-
let cachedProxiedLanguageService: tsModule.LanguageService | undefined
53-
let cachedProxiedLanguageServiceHost: tsModule.LanguageServiceHost | undefined
54-
function getProxiedLanguageService() {
55-
if (cachedProxiedLanguageService)
56-
return {
57-
languageService: cachedProxiedLanguageService as tsModule.LanguageService,
58-
languageServiceHost:
59-
cachedProxiedLanguageServiceHost as tsModule.LanguageServiceHost & {
60-
addFile: (fileName: string, body: string) => void
61-
},
62-
}
61+
function updateVirtualFileWithType(
62+
fileName: string,
63+
node: tsModule.VariableDeclaration | tsModule.FunctionDeclaration,
64+
isGenerateMetadata?: boolean
65+
) {
66+
const ts = getTs()
67+
const isFunction = ts.isFunctionDeclaration(node)
68+
const key = cacheKey(fileName, isFunction, isGenerateMetadata)
6369

64-
const languageServiceHost = getInfo().languageServiceHost
70+
if (updatedFilePositionsCache.has(key)) {
71+
return updatedFilePositionsCache.get(key)
72+
}
6573

66-
const ts = getTs()
67-
class ProxiedLanguageServiceHost implements tsModule.LanguageServiceHost {
68-
files: {
69-
[fileName: string]: { file: tsModule.IScriptSnapshot; ver: number }
70-
} = {}
71-
72-
log = () => {}
73-
trace = () => {}
74-
error = () => {}
75-
getCompilationSettings = () => languageServiceHost.getCompilationSettings()
76-
getScriptIsOpen = () => true
77-
getCurrentDirectory = () => languageServiceHost.getCurrentDirectory()
78-
getDefaultLibFileName = (o: any) =>
79-
languageServiceHost.getDefaultLibFileName(o)
80-
81-
getScriptVersion = (fileName: string) => {
82-
const file = this.files[fileName]
83-
if (!file) return languageServiceHost.getScriptVersion(fileName)
84-
return file.ver.toString()
85-
}
74+
let nodeEnd: number
8675

87-
getScriptSnapshot = (fileName: string) => {
88-
const file = this.files[fileName]
89-
if (!file) return languageServiceHost.getScriptSnapshot(fileName)
90-
return file.file
91-
}
76+
if (isFunction) {
77+
nodeEnd = node.body!.getFullStart()
78+
} else {
79+
nodeEnd = node.name.getFullStart() + node.name.getFullWidth()
80+
}
9281

93-
getScriptFileNames(): string[] {
94-
const names: Set<string> = new Set()
95-
for (var name in this.files) {
96-
if (this.files.hasOwnProperty(name)) {
97-
names.add(name)
98-
}
99-
}
100-
const files = languageServiceHost.getScriptFileNames()
101-
for (const file of files) {
102-
names.add(file)
103-
}
104-
return [...names]
105-
}
82+
// If the node is already typed, we don't need to do anything
83+
if (!isTyped(node)) {
84+
const source = getSource(fileName)
85+
if (!source) return
10686

107-
addFile(fileName: string, body: string) {
108-
const snap = ts.ScriptSnapshot.fromString(body)
109-
snap.getChangeRange = (_) => undefined
110-
const existing = this.files[fileName]
111-
if (existing) {
112-
this.files[fileName].ver++
113-
this.files[fileName].file = snap
87+
// We annotate with the type in a virtual language service
88+
const sourceText = source.getFullText()
89+
let annotation: string
90+
91+
if (isFunction) {
92+
if (isGenerateMetadata) {
93+
const isAsync = node.modifiers?.some(
94+
(m) => m.kind === ts.SyntaxKind.AsyncKeyword
95+
)
96+
annotation = isAsync ? TYPE_ANNOTATION_ASYNC : TYPE_ANNOTATION
11497
} else {
115-
this.files[fileName] = { ver: 1, file: snap }
98+
return
11699
}
100+
} else {
101+
annotation = TYPE_ANNOTATION
117102
}
118103

119-
readFile(fileName: string) {
120-
const file = this.files[fileName]
121-
return file
122-
? file.file.getText(0, file.file.getLength())
123-
: languageServiceHost.readFile(fileName)
124-
}
125-
fileExists(fileName: string) {
126-
return (
127-
this.files[fileName] !== undefined ||
128-
languageServiceHost.fileExists(fileName)
129-
)
130-
}
131-
}
104+
const newSource =
105+
sourceText.slice(0, nodeEnd) +
106+
annotation +
107+
sourceText.slice(nodeEnd) +
108+
TYPE_IMPORT
132109

133-
cachedProxiedLanguageServiceHost = new ProxiedLanguageServiceHost()
134-
cachedProxiedLanguageService = ts.createLanguageService(
135-
cachedProxiedLanguageServiceHost,
136-
ts.createDocumentRegistry()
137-
)
138-
return {
139-
languageService: cachedProxiedLanguageService as tsModule.LanguageService,
140-
languageServiceHost:
141-
cachedProxiedLanguageServiceHost as tsModule.LanguageServiceHost & {
142-
addFile: (fileName: string, body: string) => void
143-
},
144-
}
145-
}
110+
const { languageServiceHost } = getInfo()
111+
// Add the file to the virtual language service
112+
// This will trigger TypeScript to re-analyze the workspace
113+
languageServiceHost.addFile(fileName, newSource)
146114

147-
function updateVirtualFileWithType(
148-
fileName: string,
149-
node: tsModule.VariableDeclaration | tsModule.FunctionDeclaration,
150-
isGenerateMetadata?: boolean
151-
) {
152-
const source = getSource(fileName)
153-
if (!source) return
154-
155-
// We annotate with the type in a virtual language service
156-
const sourceText = source.getFullText()
157-
let nodeEnd: number
158-
let annotation: string
115+
const pos = [nodeEnd, annotation.length]
116+
updatedFilePositionsCache.set(key, pos)
159117

160-
const ts = getTs()
161-
if (ts.isFunctionDeclaration(node)) {
162-
if (isGenerateMetadata) {
163-
nodeEnd = node.body!.getFullStart()
164-
const isAsync = node.modifiers?.some(
165-
(m) => m.kind === ts.SyntaxKind.AsyncKeyword
166-
)
167-
annotation = isAsync ? TYPE_ANNOTATION_ASYNC : TYPE_ANNOTATION
168-
} else {
169-
return
170-
}
171-
} else {
172-
nodeEnd = node.name.getFullStart() + node.name.getFullWidth()
173-
annotation = TYPE_ANNOTATION
118+
return pos
174119
}
175120

176-
const newSource =
177-
sourceText.slice(0, nodeEnd) +
178-
annotation +
179-
sourceText.slice(nodeEnd) +
180-
TYPE_IMPORT
181-
const { languageServiceHost } = getProxiedLanguageService()
182-
languageServiceHost.addFile(fileName, newSource)
183-
184-
return [nodeEnd, annotation.length]
121+
return [nodeEnd, 0]
185122
}
186123

187124
function isTyped(
@@ -196,7 +133,7 @@ function proxyDiagnostics(
196133
n: tsModule.VariableDeclaration | tsModule.FunctionDeclaration
197134
) {
198135
// Get diagnostics
199-
const { languageService } = getProxiedLanguageService()
136+
const { languageService } = getInfo()
200137
const diagnostics = languageService.getSemanticDiagnostics(fileName)
201138
const source = getSource(fileName)
202139

@@ -225,26 +162,25 @@ const metadata = {
225162
filterCompletionsAtPosition(
226163
fileName: string,
227164
position: number,
228-
_options: any,
165+
options: any,
229166
prior: tsModule.WithMetadata<tsModule.CompletionInfo>
230167
) {
231168
const node = getMetadataExport(fileName, position)
232169
if (!node) return prior
233-
if (isTyped(node)) return prior
234170

235-
const ts = getTs()
171+
const { languageService } = getInfo()
236172

173+
const ts = getTs()
237174
// We annotate with the type in a virtual language service
238175
const pos = updateVirtualFileWithType(fileName, node)
239176
if (pos === undefined) return prior
240177

241178
// Get completions
242-
const { languageService } = getProxiedLanguageService()
243179
const newPos = position <= pos[0] ? position : position + pos[1]
244180
const completions = languageService.getCompletionsAtPosition(
245181
fileName,
246182
newPos,
247-
undefined
183+
options
248184
)
249185

250186
if (completions) {
@@ -465,7 +401,7 @@ const metadata = {
465401
const pos = updateVirtualFileWithType(fileName, node)
466402
if (pos === undefined) return
467403

468-
const { languageService } = getProxiedLanguageService()
404+
const { languageService } = getInfo()
469405
const newPos = position <= pos[0] ? position : position + pos[1]
470406

471407
const details = languageService.getCompletionEntryDetails(
@@ -489,7 +425,7 @@ const metadata = {
489425
const pos = updateVirtualFileWithType(fileName, node)
490426
if (pos === undefined) return
491427

492-
const { languageService } = getProxiedLanguageService()
428+
const { languageService } = getInfo()
493429
const newPos = position <= pos[0] ? position : position + pos[1]
494430
const insight = languageService.getQuickInfoAtPosition(fileName, newPos)
495431
return insight
@@ -503,7 +439,7 @@ const metadata = {
503439
// We annotate with the type in a virtual language service
504440
const pos = updateVirtualFileWithType(fileName, node)
505441
if (pos === undefined) return
506-
const { languageService } = getProxiedLanguageService()
442+
const { languageService } = getInfo()
507443
const newPos = position <= pos[0] ? position : position + pos[1]
508444

509445
const definitionInfoAndBoundSpan =

0 commit comments

Comments
 (0)