Skip to content

Commit 7dc4ebf

Browse files
kyliauKeen Yee Liau
authored andcommitted
feat: server-side implementation for ngcc feature
Ivy compiler requires Ivy-specific properties generated by ngcc to be available in the TS declaration files. As a result, the language service will have to run ngcc on project load. Since ngcc modifies node_modules, it should be run on the client side. This commit implements the feature on the server side. The idea is as follows: 1. After a project has finished loading, the server sends a notification to the client to run ngcc. Meanwhile, the server will keep the language service disabled. 2. Client runs ngcc. Once complete, client sends another notification to the server. Server re-enables language service.
1 parent a935213 commit 7dc4ebf

File tree

4 files changed

+151
-41
lines changed

4 files changed

+151
-41
lines changed

integration/lsp/ivy_spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {MessageConnection} from 'vscode-jsonrpc';
10+
11+
import {APP_COMPONENT, createConnection, initializeServer, openTextDocument} from './test_utils';
12+
13+
describe('Angular Ivy language server', () => {
14+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; /* 10 seconds */
15+
16+
let client: MessageConnection;
17+
18+
beforeEach(() => {
19+
client = createConnection({
20+
ivy: true,
21+
});
22+
client.listen();
23+
});
24+
25+
afterEach(() => {
26+
client.dispose();
27+
});
28+
29+
it('should send ngcc notification after a project has finished loading', async () => {
30+
await initializeServer(client);
31+
openTextDocument(client, APP_COMPONENT);
32+
const configFilePath = await onRunNgccNotification(client);
33+
expect(configFilePath.endsWith('integration/project/tsconfig.json')).toBeTrue();
34+
});
35+
36+
it('should disable language service until ngcc has completed', async () => {
37+
await initializeServer(client);
38+
openTextDocument(client, APP_COMPONENT);
39+
const languageServiceEnabled = await onLanguageServiceStateNotification(client);
40+
expect(languageServiceEnabled).toBeFalse();
41+
});
42+
43+
it('should re-enable language service once ngcc has completed', async () => {
44+
await initializeServer(client);
45+
openTextDocument(client, APP_COMPONENT);
46+
const configFilePath = await onRunNgccNotification(client);
47+
client.sendNotification('angular/ngccComplete', {
48+
success: true,
49+
configFilePath,
50+
});
51+
const languageServiceEnabled = await onLanguageServiceStateNotification(client);
52+
expect(languageServiceEnabled).toBeTrue();
53+
});
54+
});
55+
56+
function onRunNgccNotification(client: MessageConnection): Promise<string> {
57+
return new Promise(resolve => {
58+
// TODO(kyliau): Figure out how to import the notification type from
59+
// common/out/notifications.d.ts. Currently we cannot do this because the
60+
// TS files and JS outputs are in different trees. As a result, node module
61+
// resolution works for the former but not the latter since their relative
62+
// import paths are different.
63+
client.onNotification('angular/runNgcc', (params: {configFilePath: string}) => {
64+
resolve(params.configFilePath);
65+
});
66+
});
67+
}
68+
69+
function onLanguageServiceStateNotification(client: MessageConnection): Promise<boolean> {
70+
return new Promise(resolve => {
71+
client.onNotification(
72+
'angular/projectLanguageService', (params: {languageServiceEnabled: boolean}) => {
73+
resolve(params.languageServiceEnabled);
74+
});
75+
});
76+
}

integration/lsp/test_utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export function createConnection(serverOptions: ServerOptions): MessageConnectio
3838
env: {
3939
TSC_NONPOLLING_WATCHER: 'true',
4040
},
41+
// uncomment to debug server process
42+
// execArgv: ['--inspect-brk=9229']
4143
});
4244

4345
return createMessageConnection(

server/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const session = new Session({
3838
logger,
3939
ngPlugin: ng.name,
4040
ngProbeLocation: ng.resolvedPath,
41+
ivy: options.ivy,
4142
});
4243

4344
// Log initialization info

server/src/session.ts

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface SessionOptions {
2222
logger: Logger;
2323
ngPlugin: string;
2424
ngProbeLocation: string;
25+
ivy: boolean;
2526
}
2627

2728
enum LanguageId {
@@ -40,11 +41,13 @@ export class Session {
4041
private readonly connection: lsp.IConnection;
4142
private readonly projectService: ts.server.ProjectService;
4243
private readonly logger: Logger;
44+
private readonly ivy: boolean;
4345
private diagnosticsTimeout: NodeJS.Timeout|null = null;
4446
private isProjectLoading = false;
4547

4648
constructor(options: SessionOptions) {
4749
this.logger = options.logger;
50+
this.ivy = options.ivy;
4851
// Create a connection for the server. The connection uses Node's IPC as a transport.
4952
this.connection = lsp.createConnection();
5053
this.addProtocolHandlers(this.connection);
@@ -102,6 +105,27 @@ export class Session {
102105
conn.onTypeDefinition(p => this.onTypeDefinition(p));
103106
conn.onHover(p => this.onHover(p));
104107
conn.onCompletion(p => this.onCompletion(p));
108+
conn.onNotification(notification.NgccComplete, p => this.handleNgccNotification(p));
109+
}
110+
111+
private handleNgccNotification(params: notification.NgccCompleteParams) {
112+
const {configFilePath} = params;
113+
if (!params.success) {
114+
this.error(
115+
`Failed to run ngcc for ${configFilePath}:\n` +
116+
`${params.error}\n` +
117+
`Language service will remain disabled.`);
118+
return;
119+
}
120+
const project = this.projectService.findProject(configFilePath);
121+
if (!project) {
122+
this.error(
123+
`Failed to find project for ${configFilePath} returned by ngcc.\n` +
124+
`Language service will remain disabled.`);
125+
return;
126+
}
127+
project.enableLanguageService();
128+
this.info(`Enabling Ivy language service for ${project.projectName}.`);
105129
}
106130

107131
/**
@@ -118,23 +142,23 @@ export class Session {
118142
this.logger.info(`Loading new project: ${event.data.reason}`);
119143
break;
120144
case ts.server.ProjectLoadingFinishEvent: {
121-
const {project} = event.data;
122-
try {
123-
// Disable language service if project is not Angular
124-
this.checkIsAngularProject(project);
125-
} finally {
126-
if (this.isProjectLoading) {
127-
this.isProjectLoading = false;
128-
this.connection.sendNotification(notification.ProjectLoadingFinish);
129-
}
145+
if (this.isProjectLoading) {
146+
this.isProjectLoading = false;
147+
this.connection.sendNotification(notification.ProjectLoadingFinish);
130148
}
149+
this.checkProject(event.data.project);
131150
break;
132151
}
133152
case ts.server.ProjectsUpdatedInBackgroundEvent:
134153
// ProjectsUpdatedInBackgroundEvent is sent whenever diagnostics are
135154
// requested via project.refreshDiagnostics()
136155
this.triggerDiagnostics(event.data.openFiles);
137156
break;
157+
case ts.server.ProjectLanguageServiceStateEvent:
158+
this.connection.sendNotification(notification.ProjectLanguageService, {
159+
projectName: event.data.project.getProjectName(),
160+
languageServiceEnabled: event.data.languageServiceEnabled,
161+
});
138162
}
139163
}
140164

@@ -501,12 +525,10 @@ export class Session {
501525
}
502526

503527
/**
504-
* Determine if the specified `project` is Angular, and disable the language
505-
* service if not.
506-
* @param project
528+
* Disable the language service if the specified `project` is not Angular or
529+
* Ivy mode is enabled.
507530
*/
508-
private checkIsAngularProject(project: ts.server.Project) {
509-
const NG_CORE = '@angular/core/core.d.ts';
531+
private checkProject(project: ts.server.Project) {
510532
const {projectName} = project;
511533
if (!project.languageServiceEnabled) {
512534
this.info(
@@ -517,42 +539,51 @@ export class Session {
517539

518540
return;
519541
}
520-
if (!isAngularProject(project, NG_CORE)) {
521-
project.disableLanguageService();
522-
this.info(
523-
`Disabling language service for ${projectName} because it is not an Angular project ` +
524-
`('${NG_CORE}' could not be found). ` +
525-
`If you believe you are seeing this message in error, please reinstall the packages in your package.json.`);
526-
527-
if (project.getExcludedFiles().some(f => f.endsWith(NG_CORE))) {
528-
this.info(
529-
`Please check your tsconfig.json to make sure 'node_modules' directory is not excluded.`);
530-
}
531542

543+
if (!this.checkIsAngularProject(project)) {
532544
return;
533545
}
534546

535-
// The language service should be enabled at this point.
536-
this.info(`Enabling language service for ${projectName}.`);
547+
if (this.ivy && project instanceof ts.server.ConfiguredProject) {
548+
// Keep language service disabled until ngcc is completed.
549+
project.disableLanguageService();
550+
this.connection.sendNotification(notification.RunNgcc, {
551+
configFilePath: project.getConfigFilePath(),
552+
});
553+
} else {
554+
// Immediately enable Legacy/ViewEngine language service
555+
this.info(`Enabling VE language service for ${projectName}.`);
556+
}
537557
}
538-
}
539558

540-
/**
541-
* Return true if the specified `project` contains the Angular core declaration.
542-
* @param project
543-
* @param ngCore path that uniquely identifies `@angular/core`.
544-
*/
545-
function isAngularProject(project: ts.server.Project, ngCore: string): boolean {
546-
project.markAsDirty(); // Must mark project as dirty to rebuild the program.
547-
if (project.isNonTsProject()) {
548-
return false;
549-
}
550-
for (const fileName of project.getFileNames()) {
551-
if (fileName.endsWith(ngCore)) {
559+
/**
560+
* Determine if the specified `project` is Angular, and disable the language
561+
* service if not.
562+
*/
563+
private checkIsAngularProject(project: ts.server.Project): boolean {
564+
const {projectName} = project;
565+
const NG_CORE = '@angular/core/core.d.ts';
566+
567+
const isAngularProject = project.hasRoots() && !project.isNonTsProject() &&
568+
project.getFileNames().some(f => f.endsWith(NG_CORE));
569+
570+
if (isAngularProject) {
552571
return true;
553572
}
573+
574+
project.disableLanguageService();
575+
this.info(
576+
`Disabling language service for ${projectName} because it is not an Angular project ` +
577+
`('${NG_CORE}' could not be found). ` +
578+
`If you believe you are seeing this message in error, please reinstall the packages in your package.json.`);
579+
580+
if (project.getExcludedFiles().some(f => f.endsWith(NG_CORE))) {
581+
this.info(
582+
`Please check your tsconfig.json to make sure 'node_modules' directory is not excluded.`);
583+
}
584+
585+
return false;
554586
}
555-
return false;
556587
}
557588

558589
function isConfiguredProject(project: ts.server.Project): project is ts.server.ConfiguredProject {

0 commit comments

Comments
 (0)