Skip to content

Commit bd08771

Browse files
kyliauKeen Yee Liau
authored andcommitted
feat: client-side implementation for ngcc feature
This commit implements the client-side work to support the ngcc feature. The idea is as follows: 1. Client receives notification from server 2. Client runs ngcc 3. Client sends notification to server once ngcc is done As part of this work, we implement our own progress reporter to show each step of the ngcc compilation.
1 parent 56176b2 commit bd08771

File tree

5 files changed

+193
-31
lines changed

5 files changed

+193
-31
lines changed

client/src/command-ngcc.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 {spawn} from 'child_process';
10+
import * as path from 'path';
11+
import * as vscode from 'vscode';
12+
13+
function resolveNgccFrom(directory: string): string|null {
14+
try {
15+
return require.resolve(`@angular/compiler-cli/ngcc/main-ngcc.js`, {
16+
paths: [directory],
17+
});
18+
} catch {
19+
return null;
20+
}
21+
}
22+
23+
/**
24+
* Resolve ngcc from the directory that contains the specified `tsconfig` and
25+
* run ngcc.
26+
*/
27+
export async function resolveAndRunNgcc(
28+
tsconfig: string, progress: vscode.Progress<string>): Promise<void> {
29+
const directory = path.dirname(tsconfig);
30+
const ngcc = resolveNgccFrom(directory);
31+
if (!ngcc) {
32+
throw new Error(`Failed to resolve ngcc from ${directory}`);
33+
}
34+
const childProcess = spawn(
35+
ngcc,
36+
[
37+
'--tsconfig',
38+
tsconfig,
39+
],
40+
{
41+
cwd: directory,
42+
});
43+
44+
childProcess.stdout.on('data', (data: Buffer) => {
45+
for (let entry of data.toString().split('\n')) {
46+
entry = entry.trim();
47+
if (entry) {
48+
progress.report(entry);
49+
}
50+
}
51+
});
52+
53+
return new Promise((resolve, reject) => {
54+
childProcess.on('error', (error: Error) => {
55+
reject(error);
56+
});
57+
childProcess.on('close', (code: number) => {
58+
if (code === 0) {
59+
resolve();
60+
} else {
61+
throw new Error(`ngcc for ${tsconfig} returned exit code ${code}`);
62+
}
63+
});
64+
});
65+
}

client/src/extension.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import * as path from 'path';
1111
import * as vscode from 'vscode';
1212
import * as lsp from 'vscode-languageclient';
1313

14-
import {projectLoadingNotification} from '../../common/out/notifications';
14+
import * as notification from '../../common/out/notifications';
1515

16+
import {resolveAndRunNgcc} from './command-ngcc';
1617
import {registerCommands} from './commands';
18+
import {withProgress} from './progress-reporter';
1719

1820
export function activate(context: vscode.ExtensionContext) {
1921
// If the extension is launched in debug mode then the debug server options are used
@@ -53,29 +55,50 @@ export function activate(context: vscode.ExtensionContext) {
5355
// client can be deactivated on extension deactivation
5456
context.subscriptions.push(...registerCommands(client), client.start());
5557

56-
client.onDidChangeState((e) => {
57-
let task: {resolve: () => void}|undefined;
58-
if (e.newState == lsp.State.Running) {
59-
client.onNotification(projectLoadingNotification.start, () => {
60-
if (task) {
61-
task.resolve();
62-
task = undefined;
63-
}
64-
vscode.window.withProgress(
65-
{
66-
location: vscode.ProgressLocation.Window,
67-
title: 'Initializing Angular language features',
68-
},
69-
() => new Promise((resolve) => {
70-
task = {resolve};
71-
}));
72-
});
58+
client.onDidChangeState((e: lsp.StateChangeEvent) => {
59+
if (e.newState === lsp.State.Running) {
60+
registerNotificationHandlers(client);
61+
}
62+
});
63+
}
7364

74-
client.onNotification(projectLoadingNotification.finish, () => {
75-
if (task) {
76-
task.resolve();
77-
task = undefined;
78-
}
65+
function registerNotificationHandlers(client: lsp.LanguageClient) {
66+
client.onNotification(notification.ProjectLoadingStart, () => {
67+
vscode.window.withProgress(
68+
{
69+
location: vscode.ProgressLocation.Window,
70+
title: 'Initializing Angular language features',
71+
},
72+
() => new Promise((resolve) => {
73+
client.onNotification(notification.ProjectLoadingFinish, resolve);
74+
}),
75+
);
76+
});
77+
78+
client.onNotification(notification.RunNgcc, async (params: notification.RunNgccParams) => {
79+
const {configFilePath} = params;
80+
try {
81+
await withProgress(
82+
{
83+
location: vscode.ProgressLocation.Window,
84+
title: `Running ngcc for project ${configFilePath}`,
85+
cancellable: false,
86+
},
87+
(progress: vscode.Progress<string>) => {
88+
return resolveAndRunNgcc(configFilePath, progress);
89+
},
90+
);
91+
client.sendNotification(notification.NgccComplete, {
92+
configFilePath,
93+
success: true,
94+
});
95+
} catch (e) {
96+
vscode.window.showWarningMessage(
97+
`Failed to run ngcc. Ivy language service might not function correctly. Please see the log file for more information.`);
98+
client.sendNotification(notification.NgccComplete, {
99+
configFilePath,
100+
success: false,
101+
error: e.message,
79102
});
80103
}
81104
});

client/src/progress-reporter.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 * as vscode from 'vscode';
10+
11+
const EMPTY_DISPOSABLE = vscode.Disposable.from();
12+
13+
class ProgressReporter implements vscode.Progress<unknown> {
14+
private lastMessage: vscode.Disposable = EMPTY_DISPOSABLE;
15+
16+
report(value: unknown) {
17+
this.lastMessage.dispose(); // clear the last message
18+
// See https://code.visualstudio.com/api/references/icons-in-labels for
19+
// icons available in vscode. "~spin" animates the icon.
20+
this.lastMessage = vscode.window.setStatusBarMessage(`$(loading~spin) Angular: ${value}`);
21+
}
22+
23+
finish() {
24+
this.lastMessage.dispose();
25+
this.lastMessage = EMPTY_DISPOSABLE;
26+
}
27+
}
28+
29+
interface Task<R> {
30+
(progress: vscode.Progress<string>): Promise<R>;
31+
}
32+
33+
/**
34+
* Show progress in the editor. Progress is shown while running the given `task`
35+
* callback and while the promise it returns is in the pending state.
36+
* If the given `task` returns a rejected promise, this function will reject with
37+
* the same promise.
38+
*/
39+
export async function withProgress<R>(options: vscode.ProgressOptions, task: Task<R>): Promise<R> {
40+
// Although not strictly compatible, the signature of this function follows
41+
// the signature of vscode.window.withProgress() to make it easier to switch
42+
// to the official API if we choose to do so later.
43+
const reporter = new ProgressReporter();
44+
if (options.title) {
45+
reporter.report(options.title);
46+
}
47+
try {
48+
return await task(reporter);
49+
} finally {
50+
reporter.finish();
51+
}
52+
}

common/notifications.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,31 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {NotificationType0} from 'vscode-jsonrpc';
9+
import {NotificationType, NotificationType0} from 'vscode-jsonrpc';
1010

11-
export const projectLoadingNotification = {
12-
start: new NotificationType0('angular/projectLoadingStart'),
13-
finish: new NotificationType0('angular/projectLoadingFinish')
11+
export const ProjectLoadingStart = new NotificationType0('angular/projectLoadingStart');
12+
export const ProjectLoadingFinish = new NotificationType0('angular/projectLoadingFinish');
13+
14+
export interface ProjectLanguageServiceParams {
15+
projectName: string;
16+
languageServiceEnabled: boolean;
17+
}
18+
19+
export const ProjectLanguageService =
20+
new NotificationType<ProjectLanguageServiceParams>('angular/projectLanguageService');
21+
22+
export interface RunNgccParams {
23+
configFilePath: string;
24+
}
25+
26+
export const RunNgcc = new NotificationType<RunNgccParams>('angular/runNgcc');
27+
28+
export type NgccCompleteParams = {
29+
configFilePath: string; success: true;
30+
}|{
31+
configFilePath: string;
32+
success: false;
33+
error: string;
1434
};
35+
36+
export const NgccComplete = new NotificationType<NgccCompleteParams>('angular/ngccComplete');

server/src/session.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import * as ts from 'typescript/lib/tsserverlibrary';
1010
import * as lsp from 'vscode-languageserver';
1111

12-
import {projectLoadingNotification} from '../../common/out/notifications';
12+
import * as notification from '../../common/out/notifications';
1313

1414
import {tsCompletionEntryToLspCompletionItem} from './completion';
1515
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
@@ -114,7 +114,7 @@ export class Session {
114114
switch (event.eventName) {
115115
case ts.server.ProjectLoadingStartEvent:
116116
this.isProjectLoading = true;
117-
this.connection.sendNotification(projectLoadingNotification.start);
117+
this.connection.sendNotification(notification.ProjectLoadingStart);
118118
this.logger.info(`Loading new project: ${event.data.reason}`);
119119
break;
120120
case ts.server.ProjectLoadingFinishEvent: {
@@ -125,7 +125,7 @@ export class Session {
125125
} finally {
126126
if (this.isProjectLoading) {
127127
this.isProjectLoading = false;
128-
this.connection.sendNotification(projectLoadingNotification.finish);
128+
this.connection.sendNotification(notification.ProjectLoadingFinish);
129129
}
130130
}
131131
break;
@@ -269,7 +269,7 @@ export class Session {
269269
} catch (error) {
270270
if (this.isProjectLoading) {
271271
this.isProjectLoading = false;
272-
this.connection.sendNotification(projectLoadingNotification.finish);
272+
this.connection.sendNotification(notification.ProjectLoadingFinish);
273273
}
274274
if (error.stack) {
275275
this.error(error.stack);

0 commit comments

Comments
 (0)