Skip to content

Commit d8d117f

Browse files
committed
[WIP] typings discovery in tsserver
1 parent 0a1ec43 commit d8d117f

12 files changed

Lines changed: 362 additions & 7 deletions

Jakefile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ var harnessSources = harnessCoreSources.concat([
186186
"scriptInfo.ts",
187187
"lsHost.ts",
188188
"project.ts",
189+
"typingsCache.ts",
189190
"editorServices.ts",
190191
"protocol.d.ts",
191192
"session.ts",

src/server/editorServices.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/// <reference path="scriptVersionCache.ts"/>
77
/// <reference path="lsHost.ts"/>
88
/// <reference path="project.ts"/>
9+
/// <reference path="typingsCache.ts"/>
910

1011
namespace ts.server {
1112
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
@@ -130,6 +131,9 @@ namespace ts.server {
130131
}
131132

132133
export class ProjectService {
134+
135+
public readonly typingsCache: TypingsCache;
136+
133137
private readonly documentRegistry: DocumentRegistry;
134138

135139
/**
@@ -654,6 +658,7 @@ namespace ts.server {
654658
compilerOptions: parsedCommandLine.options,
655659
configHasFilesProperty: configObj.config["files"] !== undefined,
656660
wildcardDirectories: parsedCommandLine.wildcardDirectories,
661+
typingOptions: parsedCommandLine.typingOptions
657662
};
658663
return { success: true, projectOptions };
659664
}
@@ -697,6 +702,7 @@ namespace ts.server {
697702
this.documentRegistry,
698703
projectOptions.configHasFilesProperty,
699704
projectOptions.compilerOptions,
705+
projectOptions.typingOptions,
700706
projectOptions.wildcardDirectories,
701707
/*languageServiceEnabled*/ !sizeLimitExceeded);
702708

src/server/lsHost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ namespace ts.server {
101101
}
102102

103103
getScriptFileNames() {
104-
return this.project.getRootFiles();
104+
return this.project.getRootFilesLSHost();
105105
}
106106

107107
getScriptKind(fileName: string) {

src/server/project.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// <reference path="utilities.ts"/>
33
/// <reference path="scriptInfo.ts"/>
44
/// <reference path="lsHost.ts"/>
5+
/// <reference path="typingsCache.ts"/>
56

67
namespace ts.server {
78

@@ -25,7 +26,6 @@ namespace ts.server {
2526
private program: ts.Program;
2627

2728
private languageService: LanguageService;
28-
2929
/**
3030
* Set of files that was returned from the last call to getChangesSinceVersion.
3131
*/
@@ -47,6 +47,8 @@ namespace ts.server {
4747
*/
4848
private projectStateVersion = 0;
4949

50+
private typingFiles: string[];
51+
5052
constructor(
5153
readonly projectKind: ProjectKind,
5254
readonly projectService: ProjectService,
@@ -74,7 +76,7 @@ namespace ts.server {
7476
this.markAsDirty();
7577
}
7678

77-
getLanguageService(ensureSynchronized = true): LanguageService {
79+
getLanguageService(ensureSynchronized = true): LanguageService {
7880
if (ensureSynchronized) {
7981
this.updateGraph();
8082
}
@@ -136,6 +138,21 @@ namespace ts.server {
136138
return this.rootFiles && this.rootFiles.map(info => info.fileName);
137139
}
138140

141+
getRootFilesLSHost() {
142+
const result: string[] = [];
143+
if (this.rootFiles) {
144+
for (const f of this.rootFiles) {
145+
result.push(f.fileName);
146+
}
147+
if (this.typingFiles) {
148+
for (const f of this.typingFiles) {
149+
result.push(f);
150+
}
151+
}
152+
}
153+
return result;
154+
}
155+
139156
getRootScriptInfos() {
140157
return this.rootFiles;
141158
}
@@ -209,16 +226,35 @@ namespace ts.server {
209226
if (!this.languageServiceEnabled) {
210227
return true;
211228
}
229+
const hasChanges = this.updateGraphWorker();
230+
if (hasChanges) {
231+
if (this.setTypings(this.projectService.typingsCache.getTypingsForProject(this))) {
232+
this.updateGraphWorker();
233+
}
234+
this.projectStructureVersion++;
235+
}
236+
return hasChanges;
237+
}
238+
239+
setTypings(typings: string[]): boolean {
240+
if (typings === this.typingFiles) {
241+
return false;
242+
}
243+
this.typingFiles = typings;
244+
this.markAsDirty();
245+
return true;
246+
}
212247

248+
private updateGraphWorker() {
213249
const oldProgram = this.program;
214250
this.program = this.languageService.getProgram();
215251

216-
const oldProjectStructureVersion = this.projectStructureVersion;
252+
let hasChanges = false;
217253
// bump up the version if
218254
// - oldProgram is not set - this is a first time updateGraph is called
219255
// - newProgram is different from the old program and structure of the old program was not reused.
220256
if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) {
221-
this.projectStructureVersion++;
257+
hasChanges = true;
222258
if (oldProgram) {
223259
for (const f of oldProgram.getSourceFiles()) {
224260
if (this.program.getSourceFileByPath(f.path)) {
@@ -232,8 +268,7 @@ namespace ts.server {
232268
}
233269
}
234270
}
235-
236-
return oldProjectStructureVersion === this.projectStructureVersion;
271+
return hasChanges;
237272
}
238273

239274
getScriptInfoLSHost(fileName: string) {
@@ -392,11 +427,16 @@ namespace ts.server {
392427
documentRegistry: ts.DocumentRegistry,
393428
hasExplicitListOfFiles: boolean,
394429
compilerOptions: CompilerOptions,
430+
private typingOptions: TypingOptions,
395431
private wildcardDirectories: Map<WatchDirectoryFlags>,
396432
languageServiceEnabled: boolean) {
397433
super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions);
398434
}
399435

436+
getTypingOptions() {
437+
return this.typingOptions;
438+
}
439+
400440
getProjectName() {
401441
return this.configFileName;
402442
}

src/server/session.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ namespace ts.server {
147147
}
148148

149149
export class Session {
150+
private readonly gcTimer: GcTimer;
150151
protected projectService: ProjectService;
151152
private errorTimer: any; /*NodeJS.Timer | number*/
152153
private immediateId: any;
@@ -165,6 +166,7 @@ namespace ts.server {
165166
new ProjectService(host, logger, cancellationToken, useSingleInferredProject, (eventName, project, fileName) => {
166167
this.handleEvent(eventName, project, fileName);
167168
});
169+
this.gcTimer = new GcTimer(host, /*delay*/ 15000, logger);
168170
}
169171

170172
private handleEvent(eventName: string, project: Project, fileName: NormalizedPath) {
@@ -1464,6 +1466,7 @@ namespace ts.server {
14641466
}
14651467

14661468
public onMessage(message: string) {
1469+
this.gcTimer.scheduleCollect();
14671470
let start: number[];
14681471
if (this.logger.hasLevel(LogLevel.requestTime)) {
14691472
start = this.hrtime();

src/server/tsconfig.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"files": [
1616
"../services/shims.ts",
1717
"../services/utilities.ts",
18+
"utilities.ts",
19+
"scriptVersionCache.ts",
20+
"scriptInfo.ts",
21+
"lshost.ts",
22+
"project.ts",
1823
"editorServices.ts",
1924
"protocol.d.ts",
2025
"session.ts",

src/server/tsconfig.library.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
"types": []
1111
},
1212
"files": [
13+
"../services/shims.ts",
14+
"../services/utilities.ts",
15+
"utilities.ts",
16+
"scriptVersionCache.ts",
17+
"scriptInfo.ts",
18+
"lshost.ts",
19+
"project.ts",
1320
"editorServices.ts",
1421
"protocol.d.ts",
1522
"session.ts"

src/server/typingsCache.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/// <reference path="project.ts"/>
2+
3+
namespace ts.server {
4+
export interface ITypingsInstaller {
5+
enqueueInstallTypingsRequest(p: Project): void;
6+
}
7+
8+
class TypingsCacheEntry {
9+
readonly typingOptions: TypingOptions;
10+
readonly compilerOptions: CompilerOptions;
11+
readonly typings: Path[];
12+
}
13+
14+
const emptyArray: any[] = [];
15+
const jsOrDts = [".js", ".d.ts"];
16+
17+
function getTypingOptionsForProjects(proj: Project): TypingOptions {
18+
if (proj.projectKind === ProjectKind.Configured) {
19+
return (<ConfiguredProject>proj).getTypingOptions();
20+
}
21+
22+
const enableAutoDiscovery =
23+
proj.projectKind === ProjectKind.Inferred &&
24+
proj.getCompilerOptions().allowJs &&
25+
proj.getFileNames().every(f => fileExtensionIsAny(f, jsOrDts));
26+
27+
// TODO: add .d.ts files to excludes
28+
return { enableAutoDiscovery, include: emptyArray, exclude: emptyArray };
29+
}
30+
31+
function setIsEqualTo(arr1: string[], arr2: string[]): boolean {
32+
if (arr1 === arr2) {
33+
return true;
34+
}
35+
if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) {
36+
return true;
37+
}
38+
const set: Map<boolean> = Object.create(null);
39+
let unique = 0;
40+
41+
for (const v of arr1) {
42+
if (set[v] !== true) {
43+
set[v] = true;
44+
unique++;
45+
}
46+
}
47+
for (const v of arr2) {
48+
if (!hasProperty(set, v)) {
49+
return false;
50+
}
51+
if (set[v] === true) {
52+
set[v] = false;
53+
unique--;
54+
}
55+
}
56+
return unique === 0;
57+
}
58+
59+
function typingOptionsChanged(opt1: TypingOptions, opt2: TypingOptions): boolean {
60+
return opt1.enableAutoDiscovery !== opt2.enableAutoDiscovery ||
61+
!setIsEqualTo(opt1.include, opt2.include) ||
62+
!setIsEqualTo(opt1.exclude, opt2.exclude);
63+
}
64+
65+
function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean {
66+
// TODO: add more relevant properties
67+
return opt1.allowJs != opt2.allowJs;
68+
}
69+
70+
export class TypingsCache {
71+
private readonly perProjectCache: Map<TypingsCacheEntry> = {};
72+
73+
constructor(private readonly installer: ITypingsInstaller) {
74+
}
75+
76+
getTypingsForProject(project: Project): Path[] {
77+
const typingOptions = getTypingOptionsForProjects(project);
78+
79+
if(!typingOptions.enableAutoDiscovery) {
80+
return emptyArray;
81+
}
82+
83+
const entry = this.perProjectCache[project.getProjectName()];
84+
if (!entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) {
85+
this.installer.enqueueInstallTypingsRequest(project);
86+
}
87+
return entry? entry.typings : emptyArray;
88+
}
89+
90+
deleteTypingsForProject(project: Project) {
91+
delete this.perProjectCache[project.getProjectName()];
92+
}
93+
}
94+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// <reference path="typingsInstaller.ts"/>
2+
3+
namespace ts.server.typingsInstaller {
4+
export class NodeTypingsInstaller extends TypingsInstaller {
5+
private execSync: { (command: string, options: { stdio: "ignore" }): any };
6+
constructor() {
7+
super();
8+
this.execSync = require("child_process").execSync;
9+
}
10+
11+
init() {
12+
super.init();
13+
process.on("install", (req: InstallTypingsRequest) => {
14+
this.install(req);
15+
})
16+
}
17+
18+
protected isPackageInstalled(packageName: string) {
19+
try {
20+
this.execSync(`npm list --global --depth=1 ${name}`, { stdio: "ignore" });
21+
return true;
22+
}
23+
catch (e) {
24+
return false;
25+
}
26+
}
27+
28+
protected installPackage(packageName: string) {
29+
try {
30+
this.execSync(`npm install --global ${name}`, { stdio: "ignore" });
31+
return true;
32+
}
33+
catch (e) {
34+
return false;
35+
}
36+
}
37+
38+
protected getTypingResolutionHost() {
39+
return sys;
40+
}
41+
42+
protected sendResponse(response: InstallTypingsResponse) {
43+
process.send(response);
44+
}
45+
}
46+
47+
const installer = new NodeTypingsInstaller();
48+
installer.init();
49+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"noImplicitAny": true,
4+
"noImplicitThis": true,
5+
"removeComments": true,
6+
"preserveConstEnums": true,
7+
"pretty": true,
8+
"out": "../../../built/local/typingsInstaller.js",
9+
"sourceMap": true,
10+
"stripInternal": true,
11+
"types": [
12+
"node"
13+
]
14+
},
15+
"files": [
16+
"../../services/services.ts",
17+
"../utilities.ts",
18+
"typingsInstaller.ts",
19+
"nodeTypingsInstaller.ts"
20+
]
21+
}

0 commit comments

Comments
 (0)