Skip to content

Commit 3119f42

Browse files
committed
wip
1 parent 5d62d79 commit 3119f42

File tree

4 files changed

+57
-149
lines changed

4 files changed

+57
-149
lines changed

packages/next/errors.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -665,5 +665,6 @@
665665
"664": "Missing 'next-action' header.",
666666
"665": "Failed to find Server Action \"%s\". This request might be from an older or newer deployment.\\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action",
667667
"666": "Turbopack builds are only available in canary builds of Next.js.",
668-
"667": "receiveExpiredTags is deprecated, and not expected to be called."
668+
"667": "receiveExpiredTags is deprecated, and not expected to be called.",
669+
"668": "[next] Failed to create virtual TypeScript environment."
669670
}

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

+14-15
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,6 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
3232
typescript: ts,
3333
}) => {
3434
function create(info: tsModule.server.PluginCreateInfo) {
35-
// Set up decorator object
36-
const proxy = Object.create(null)
37-
for (let k of Object.keys(info.languageService)) {
38-
const x = (info.languageService as any)[k]
39-
proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args)
40-
}
41-
4235
// Get plugin options
4336
// config is the plugin options from the user's tsconfig.json
4437
// e.g. { "plugins": [{ "name": "next", "enabled": true }] }
@@ -47,16 +40,16 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
4740
const isPluginEnabled = info.config.enabled ?? true
4841

4942
if (!isPluginEnabled) {
50-
return proxy
43+
return info.languageService
5144
}
5245

53-
init({
46+
const env = init({
5447
ts,
5548
info,
5649
})
5750

5851
// Auto completion
59-
proxy.getCompletionsAtPosition = (
52+
env.languageService.getCompletionsAtPosition = (
6053
fileName: string,
6154
position: number,
6255
options: any
@@ -114,7 +107,7 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
114107
}
115108

116109
// Show auto completion details
117-
proxy.getCompletionEntryDetails = (
110+
env.languageService.getCompletionEntryDetails = (
118111
fileName: string,
119112
position: number,
120113
entryName: string,
@@ -152,7 +145,10 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
152145
}
153146

154147
// Quick info
155-
proxy.getQuickInfoAtPosition = (fileName: string, position: number) => {
148+
env.languageService.getQuickInfoAtPosition = (
149+
fileName: string,
150+
position: number
151+
) => {
156152
const prior = info.languageService.getQuickInfoAtPosition(
157153
fileName,
158154
position
@@ -184,7 +180,7 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
184180
}
185181

186182
// Show errors for disallowed imports
187-
proxy.getSemanticDiagnostics = (fileName: string) => {
183+
env.languageService.getSemanticDiagnostics = (fileName: string) => {
188184
const prior = info.languageService.getSemanticDiagnostics(fileName)
189185
const source = getSource(fileName)
190186
if (!source) return prior
@@ -364,7 +360,10 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
364360
}
365361

366362
// Get definition and link for specific node
367-
proxy.getDefinitionAndBoundSpan = (fileName: string, position: number) => {
363+
env.languageService.getDefinitionAndBoundSpan = (
364+
fileName: string,
365+
position: number
366+
) => {
368367
const entryInfo = getEntryInfo(fileName)
369368
if (isAppEntryFile(fileName) && !entryInfo.client) {
370369
const metadataDefinition = metadata.getDefinitionAndBoundSpan(
@@ -377,7 +376,7 @@ export const createTSPlugin: tsModule.server.PluginModuleFactory = ({
377376
return info.languageService.getDefinitionAndBoundSpan(fileName, position)
378377
}
379378

380-
return proxy
379+
return env.languageService
381380
}
382381

383382
return { create }

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

+26-120
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NEXT_TS_ERRORS } from '../constant'
22
import {
3-
getInfo,
43
getSource,
54
getTs,
65
getTypeChecker,
76
isPositionInsideNode,
7+
log,
8+
virtualTsEnv,
89
} from '../utils'
910

1011
import type tsModule from 'typescript/lib/tsserverlibrary'
@@ -49,101 +50,6 @@ function getMetadataExport(fileName: string, position: number) {
4950
return metadataExport
5051
}
5152

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-
}
63-
64-
const languageServiceHost = getInfo().languageServiceHost
65-
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-
}
86-
87-
getScriptSnapshot = (fileName: string) => {
88-
const file = this.files[fileName]
89-
if (!file) return languageServiceHost.getScriptSnapshot(fileName)
90-
return file.file
91-
}
92-
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-
}
106-
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
114-
} else {
115-
this.files[fileName] = { ver: 1, file: snap }
116-
}
117-
}
118-
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-
}
132-
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-
}
146-
14753
function updateVirtualFileWithType(
14854
fileName: string,
14955
node: tsModule.VariableDeclaration | tsModule.FunctionDeclaration,
@@ -178,8 +84,14 @@ function updateVirtualFileWithType(
17884
annotation +
17985
sourceText.slice(nodeEnd) +
18086
TYPE_IMPORT
181-
const { languageServiceHost } = getProxiedLanguageService()
182-
languageServiceHost.addFile(fileName, newSource)
87+
88+
if (virtualTsEnv.sys.fileExists(fileName)) {
89+
log('Updating file: ' + fileName)
90+
virtualTsEnv.updateFile(fileName, newSource)
91+
} else {
92+
log('Creating file: ' + fileName)
93+
virtualTsEnv.createFile(fileName, newSource)
94+
}
18395

18496
return [nodeEnd, annotation.length]
18597
}
@@ -196,8 +108,9 @@ function proxyDiagnostics(
196108
n: tsModule.VariableDeclaration | tsModule.FunctionDeclaration
197109
) {
198110
// Get diagnostics
199-
const { languageService } = getProxiedLanguageService()
200-
const diagnostics = languageService.getSemanticDiagnostics(fileName)
111+
const diagnostics = virtualTsEnv.languageService.getSemanticDiagnostics.bind(
112+
virtualTsEnv.languageService
113+
)(fileName)
201114
const source = getSource(fileName)
202115

203116
// Filter and map the results
@@ -239,13 +152,11 @@ const metadata = {
239152
if (pos === undefined) return prior
240153

241154
// Get completions
242-
const { languageService } = getProxiedLanguageService()
243155
const newPos = position <= pos[0] ? position : position + pos[1]
244-
const completions = languageService.getCompletionsAtPosition(
245-
fileName,
246-
newPos,
247-
undefined
248-
)
156+
const completions =
157+
virtualTsEnv.languageService.getCompletionsAtPosition.bind(
158+
virtualTsEnv.languageService
159+
)(fileName, newPos, undefined)
249160

250161
if (completions) {
251162
completions.isIncomplete = true
@@ -465,18 +376,11 @@ const metadata = {
465376
const pos = updateVirtualFileWithType(fileName, node)
466377
if (pos === undefined) return
467378

468-
const { languageService } = getProxiedLanguageService()
469379
const newPos = position <= pos[0] ? position : position + pos[1]
470380

471-
const details = languageService.getCompletionEntryDetails(
472-
fileName,
473-
newPos,
474-
entryName,
475-
formatOptions,
476-
source,
477-
preferences,
478-
data
479-
)
381+
const details = virtualTsEnv.languageService.getCompletionEntryDetails.bind(
382+
virtualTsEnv.languageService
383+
)(fileName, newPos, entryName, formatOptions, source, preferences, data)
480384
return details
481385
},
482386

@@ -489,9 +393,10 @@ const metadata = {
489393
const pos = updateVirtualFileWithType(fileName, node)
490394
if (pos === undefined) return
491395

492-
const { languageService } = getProxiedLanguageService()
493396
const newPos = position <= pos[0] ? position : position + pos[1]
494-
const insight = languageService.getQuickInfoAtPosition(fileName, newPos)
397+
const insight = virtualTsEnv.languageService.getQuickInfoAtPosition.bind(
398+
virtualTsEnv.languageService
399+
)(fileName, newPos)
495400
return insight
496401
},
497402

@@ -503,11 +408,12 @@ const metadata = {
503408
// We annotate with the type in a virtual language service
504409
const pos = updateVirtualFileWithType(fileName, node)
505410
if (pos === undefined) return
506-
const { languageService } = getProxiedLanguageService()
507411
const newPos = position <= pos[0] ? position : position + pos[1]
508412

509413
const definitionInfoAndBoundSpan =
510-
languageService.getDefinitionAndBoundSpan(fileName, newPos)
414+
virtualTsEnv.languageService.getDefinitionAndBoundSpan.bind(
415+
virtualTsEnv.languageService
416+
)(fileName, newPos)
511417

512418
if (definitionInfoAndBoundSpan) {
513419
// Adjust the start position of the text span

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

+15-13
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,54 @@ import type tsModule from 'typescript/lib/tsserverlibrary'
1010
type TypeScript = typeof import('typescript/lib/tsserverlibrary')
1111

1212
let ts: TypeScript
13-
let info: tsModule.server.PluginCreateInfo
1413
let appDirRegExp: RegExp
1514
export let virtualTsEnv: VirtualTypeScriptEnvironment
1615

17-
export function log(message: string) {
18-
info.project.projectService.logger.info(message)
19-
}
16+
export let log: (message: string) => void
2017

2118
// This function has to be called initially.
22-
export function init(opts: { ts: TypeScript; info: tsModule.server.PluginCreateInfo }) {
19+
export function init(opts: {
20+
ts: TypeScript
21+
info: tsModule.server.PluginCreateInfo
22+
}) {
2323
const projectDir = opts.info.project.getCurrentDirectory()
2424
ts = opts.ts
25-
info = opts.info
2625
appDirRegExp = new RegExp(
2726
'^' + (projectDir + '(/src)?/app').replace(/[\\/]/g, '[\\/]')
2827
)
28+
log = opts.info.project.projectService.logger.info
2929

3030
log('[next] Initializing Next.js TypeScript plugin at ' + projectDir)
3131

3232
const fsMap = new Map<string, string>()
3333
const system = createSystem(fsMap)
34-
const compilerOptions = info.project.getCompilerOptions()
34+
const compilerOptions = opts.info.project.getCompilerOptions()
3535
virtualTsEnv = createVirtualTypeScriptEnvironment(
3636
system,
3737
[],
3838
ts,
3939
compilerOptions
4040
)
4141

42+
if (!virtualTsEnv) {
43+
throw new Error('[next] Failed to create virtual TypeScript environment.')
44+
}
45+
4246
log('[next] Successfully initialized Next.js TypeScript plugin!')
47+
48+
return virtualTsEnv
4349
}
4450

4551
export function getTs() {
4652
return ts
4753
}
4854

49-
export function getInfo() {
50-
return info
51-
}
52-
5355
export function getTypeChecker() {
54-
return info.languageService.getProgram()?.getTypeChecker()
56+
return virtualTsEnv.languageService.getProgram()?.getTypeChecker()
5557
}
5658

5759
export function getSource(fileName: string) {
58-
return info.languageService.getProgram()?.getSourceFile(fileName)
60+
return virtualTsEnv.getSourceFile(fileName)
5961
}
6062

6163
export function removeStringQuotes(str: string): string {

0 commit comments

Comments
 (0)