Skip to content

Commit 984ecf6

Browse files
authored
fix(client,server): Correctly use bundled version unless tsdk is specified (#1970)
* refactor(client): Move server options to client class This means not having to pass context around and will also give those functions access to other things directly like outputChannel for logging. * refactor(client): Log warning to the output channel for when block syntax is disabled We should log this somewhere or there would be no way to know when this happened to a user. * refactor(client): Inline server options rather than helper function A function with a flag is frequently a code smell and in this case it is. There isn't much code shared between the two modes so inlining is better. * fix(client,server): Correctly use bundled version unless tsdk is specified When the `typescript.tsdk` is empty, it should be ignored. Instead, the logic previously would _always_ add an empty string tsdk to the beginning of the probe locations. This would result in resolving the ts server from the user's node modules rather than prefering the bundled version as intended unless a different tsdk is specifically set in the config. Additionally, if the workspace has many roots, the first one would be seen as a tsdk. While this would probably still result in the correct behavior, the code is much easier to follow when the tsdk is a separate option passed to the server. * refactor(server): Update tsserver version log to reflect actual version used The version that's currently logged is whatever the resolve logic gives. However, this resolve logic actually only affects the version when the banner is used to override the require statement. This won't happen when debugging the extension and stumped me for quite some time since the log version was using the tsdk I specified but I was not seeing the behavior I expected with that version (I was instead seeing the behavior expected from the bundled version).
1 parent ced5e24 commit 984ecf6

File tree

6 files changed

+105
-103
lines changed

6 files changed

+105
-103
lines changed

client/src/client.ts

Lines changed: 92 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,47 @@ export class AngularLanguageClient implements vscode.Disposable {
217217
throw new Error(`An existing client is running. Call stop() first.`);
218218
}
219219

220+
// Node module for the language server
221+
const args = this.constructArgs();
222+
const prodBundle = this.context.asAbsolutePath('server');
223+
const devBundle =
224+
this.context.asAbsolutePath(path.join('bazel-bin', 'server', 'src', 'server.js'));
225+
220226
// If the extension is launched in debug mode then the debug server options are used
221227
// Otherwise the run options are used
222228
const serverOptions: lsp.ServerOptions = {
223-
run: getServerOptions(this.context, false /* debug */),
224-
debug: getServerOptions(this.context, true /* debug */),
229+
run: {
230+
module: this.context.asAbsolutePath('server'),
231+
transport: lsp.TransportKind.ipc,
232+
args,
233+
},
234+
debug: {
235+
// VS Code Insider launches extensions in debug mode by default but users
236+
// install prod bundle so we have to check whether dev bundle exists.
237+
module: fs.existsSync(devBundle) ? devBundle : prodBundle,
238+
transport: lsp.TransportKind.ipc,
239+
options: {
240+
// Argv options for Node.js
241+
execArgv: [
242+
// do not lazily evaluate the code so all breakpoints are respected
243+
'--nolazy',
244+
// If debugging port is changed, update .vscode/launch.json as well
245+
'--inspect=6009',
246+
],
247+
env: {
248+
NG_DEBUG: true,
249+
}
250+
},
251+
args
252+
},
225253
};
226254

255+
if (!extensionVersionCompatibleWithAllProjects(serverOptions.run.module)) {
256+
vscode.window.showWarningMessage(
257+
`A project in the workspace is using a newer version of Angular than the language service extension. ` +
258+
`This may cause the extension to show incorrect diagnostics.`);
259+
}
260+
227261
// Create the language client and start the client.
228262
const forceDebug = process.env['NG_DEBUG'] === 'true';
229263
this.client = new lsp.LanguageClient(
@@ -242,6 +276,62 @@ export class AngularLanguageClient implements vscode.Disposable {
242276
this.disposables.push(registerNotificationHandlers(this.client));
243277
}
244278

279+
/**
280+
* Construct the arguments that's used to spawn the server process.
281+
* @param ctx vscode extension context
282+
*/
283+
private constructArgs(): string[] {
284+
const config = vscode.workspace.getConfiguration();
285+
const args: string[] = ['--logToConsole'];
286+
287+
const ngLog: string = config.get('angular.log', 'off');
288+
if (ngLog !== 'off') {
289+
// Log file does not yet exist on disk. It is up to the server to create the file.
290+
const logFile = path.join(this.context.logUri.fsPath, 'nglangsvc.log');
291+
args.push('--logFile', logFile);
292+
args.push('--logVerbosity', ngLog);
293+
}
294+
295+
const ngProbeLocations = getProbeLocations(this.context.extensionPath);
296+
args.push('--ngProbeLocations', ngProbeLocations.join(','));
297+
298+
const includeAutomaticOptionalChainCompletions =
299+
config.get<boolean>('angular.suggest.includeAutomaticOptionalChainCompletions');
300+
if (includeAutomaticOptionalChainCompletions) {
301+
args.push('--includeAutomaticOptionalChainCompletions');
302+
}
303+
304+
const includeCompletionsWithSnippetText =
305+
config.get<boolean>('angular.suggest.includeCompletionsWithSnippetText');
306+
if (includeCompletionsWithSnippetText) {
307+
args.push('--includeCompletionsWithSnippetText');
308+
}
309+
310+
const angularVersions = getAngularVersionsInWorkspace();
311+
// Only disable block syntax if we find angular/core and every one we find does not support
312+
// block syntax
313+
if (angularVersions.size > 0 && Array.from(angularVersions).every(v => v.version.major < 17)) {
314+
args.push('--disableBlockSyntax');
315+
this.outputChannel.appendLine(
316+
`All workspace roots are using versions of Angular that do not support control flow block syntax.` +
317+
` Block syntax parsing in templates will be disabled.`);
318+
}
319+
320+
const forceStrictTemplates = config.get<boolean>('angular.forceStrictTemplates');
321+
if (forceStrictTemplates) {
322+
args.push('--forceStrictTemplates');
323+
}
324+
325+
const tsdk = config.get('typescript.tsdk', '');
326+
if (tsdk.trim().length > 0) {
327+
args.push('--tsdk', tsdk);
328+
}
329+
const tsProbeLocations = [...getProbeLocations(this.context.extensionPath)];
330+
args.push('--tsProbeLocations', tsProbeLocations.join(','));
331+
332+
return args;
333+
}
334+
245335
/**
246336
* Kill the language client and perform some clean ups.
247337
*/
@@ -401,97 +491,6 @@ function getProbeLocations(bundled: string): string[] {
401491
return locations;
402492
}
403493

404-
/**
405-
* Construct the arguments that's used to spawn the server process.
406-
* @param ctx vscode extension context
407-
*/
408-
function constructArgs(ctx: vscode.ExtensionContext): string[] {
409-
const config = vscode.workspace.getConfiguration();
410-
const args: string[] = ['--logToConsole'];
411-
412-
const ngLog: string = config.get('angular.log', 'off');
413-
if (ngLog !== 'off') {
414-
// Log file does not yet exist on disk. It is up to the server to create the file.
415-
const logFile = path.join(ctx.logUri.fsPath, 'nglangsvc.log');
416-
args.push('--logFile', logFile);
417-
args.push('--logVerbosity', ngLog);
418-
}
419-
420-
const ngProbeLocations = getProbeLocations(ctx.extensionPath);
421-
args.push('--ngProbeLocations', ngProbeLocations.join(','));
422-
423-
const includeAutomaticOptionalChainCompletions =
424-
config.get<boolean>('angular.suggest.includeAutomaticOptionalChainCompletions');
425-
if (includeAutomaticOptionalChainCompletions) {
426-
args.push('--includeAutomaticOptionalChainCompletions');
427-
}
428-
429-
const includeCompletionsWithSnippetText =
430-
config.get<boolean>('angular.suggest.includeCompletionsWithSnippetText');
431-
if (includeCompletionsWithSnippetText) {
432-
args.push('--includeCompletionsWithSnippetText');
433-
}
434-
435-
const angularVersions = getAngularVersionsInWorkspace();
436-
// Only disable block syntax if we find angular/core and every one we find does not support block
437-
// syntax
438-
if (angularVersions.size > 0 && Array.from(angularVersions).every(v => v.version.major < 17)) {
439-
args.push('--disableBlockSyntax');
440-
}
441-
442-
const forceStrictTemplates = config.get<boolean>('angular.forceStrictTemplates');
443-
if (forceStrictTemplates) {
444-
args.push('--forceStrictTemplates');
445-
}
446-
447-
const tsdk: string|null = config.get('typescript.tsdk', null);
448-
const tsProbeLocations = [tsdk, ...getProbeLocations(ctx.extensionPath)];
449-
args.push('--tsProbeLocations', tsProbeLocations.join(','));
450-
451-
return args;
452-
}
453-
454-
function getServerOptions(ctx: vscode.ExtensionContext, debug: boolean): lsp.NodeModule {
455-
// Environment variables for server process
456-
const prodEnv = {};
457-
const devEnv = {
458-
...prodEnv,
459-
NG_DEBUG: true,
460-
};
461-
462-
// Node module for the language server
463-
const args = constructArgs(ctx);
464-
const prodBundle = ctx.asAbsolutePath('server');
465-
const devBundle = ctx.asAbsolutePath(path.join('bazel-bin', 'server', 'src', 'server.js'));
466-
// VS Code Insider launches extensions in debug mode by default but users
467-
// install prod bundle so we have to check whether dev bundle exists.
468-
const latestServerModule = debug && fs.existsSync(devBundle) ? devBundle : prodBundle;
469-
470-
if (!extensionVersionCompatibleWithAllProjects(latestServerModule)) {
471-
vscode.window.showWarningMessage(
472-
`A project in the workspace is using a newer version of Angular than the language service extension. ` +
473-
`This may cause the extension to show incorrect diagnostics.`);
474-
}
475-
476-
// Argv options for Node.js
477-
const prodExecArgv: string[] = [];
478-
const devExecArgv: string[] = [
479-
// do not lazily evaluate the code so all breakpoints are respected
480-
'--nolazy',
481-
// If debugging port is changed, update .vscode/launch.json as well
482-
'--inspect=6009',
483-
];
484-
485-
return {
486-
module: latestServerModule,
487-
transport: lsp.TransportKind.ipc,
488-
args,
489-
options: {
490-
env: debug ? devEnv : prodEnv,
491-
execArgv: debug ? devExecArgv : prodExecArgv,
492-
},
493-
};
494-
}
495494

496495
function extensionVersionCompatibleWithAllProjects(serverModuleLocation: string): boolean {
497496
const languageServiceVersion =

server/src/banner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export function requireOverride(moduleName: string) {
3131
throw new Error(`Import '${TSSERVER}' instead of 'typescript'`);
3232
}
3333
if (moduleName === TSSERVER) {
34-
const {tsProbeLocations} = parseCommandLine(process.argv);
35-
moduleName = resolveTsServer(tsProbeLocations).resolvedPath;
34+
const {tsProbeLocations, tsdk} = parseCommandLine(process.argv);
35+
moduleName = resolveTsServer(tsProbeLocations, tsdk).resolvedPath;
3636
}
3737
return originalRequire(moduleName);
3838
}

server/src/cmdline_utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface CommandLineOptions {
3333
logToConsole: boolean;
3434
ngProbeLocations: string[];
3535
tsProbeLocations: string[];
36+
tsdk: string|null;
3637
includeAutomaticOptionalChainCompletions: boolean;
3738
includeCompletionsWithSnippetText: boolean;
3839
forceStrictTemplates: boolean;
@@ -47,6 +48,7 @@ export function parseCommandLine(argv: string[]): CommandLineOptions {
4748
logToConsole: hasArgument(argv, '--logToConsole'),
4849
ngProbeLocations: parseStringArray(argv, '--ngProbeLocations'),
4950
tsProbeLocations: parseStringArray(argv, '--tsProbeLocations'),
51+
tsdk: findArgument(argv, '--tsdk') ?? null,
5052
includeAutomaticOptionalChainCompletions:
5153
hasArgument(argv, '--includeAutomaticOptionalChainCompletions'),
5254
includeCompletionsWithSnippetText: hasArgument(argv, '--includeCompletionsWithSnippetText'),

server/src/server.ts

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

9+
import {version as tsServerVersion} from 'typescript/lib/tsserverlibrary';
10+
911
import {generateHelpMessage, parseCommandLine} from './cmdline_utils';
1012

1113
// Parse command line arguments
@@ -27,7 +29,7 @@ function main() {
2729
logVerbosity: options.logVerbosity,
2830
});
2931

30-
const ts = resolveTsServer(options.tsProbeLocations);
32+
const ts = resolveTsServer(options.tsProbeLocations, options.tsdk);
3133
const ng = resolveNgLangSvc(options.ngProbeLocations);
3234

3335
const isG3 = ts.resolvedPath.includes('/google3/');
@@ -51,7 +53,7 @@ function main() {
5153

5254
// Log initialization info
5355
session.info(`Angular language server process ID: ${process.pid}`);
54-
session.info(`Using ${ts.name} v${ts.version} from ${ts.resolvedPath}`);
56+
session.info(`Imported typescript/lib/tsserverlibrary is version ${tsServerVersion}.`);
5557
session.info(`Using ${ng.name} v${ng.version} from ${ng.resolvedPath}`);
5658
if (logger.loggingEnabled()) {
5759
session.info(`Log file: ${logger.getLogFileName()}`);

server/src/tests/version_provider_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('Node Module Resolver', () => {
1414
const probeLocations = [__dirname];
1515

1616
it('should be able to resolve tsserver', () => {
17-
const result = resolveTsServer(probeLocations);
17+
const result = resolveTsServer(probeLocations, null);
1818
expect(result).toBeDefined();
1919
expect(result.resolvedPath).toMatch(/typescript\/lib\/tsserverlibrary.js$/);
2020
});
@@ -23,7 +23,7 @@ describe('Node Module Resolver', () => {
2323
// Resolve relative to cwd.
2424
const absPath = resolve('node_modules/typescript/lib');
2525
expect(isAbsolute(absPath)).toBeTrue();
26-
const result = resolveTsServer([absPath]);
26+
const result = resolveTsServer(probeLocations, absPath);
2727
expect(result.resolvedPath.endsWith('typescript/lib/tsserverlibrary.js')).toBeTrue();
2828
});
2929

server/src/version_provider.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,9 @@ function resolveWithMinVersion(
4747
* Resolve `typescript/lib/tsserverlibrary` from the given locations.
4848
* @param probeLocations
4949
*/
50-
export function resolveTsServer(probeLocations: string[]): NodeModule {
51-
if (probeLocations.length > 0) {
52-
// The first probe location is `typescript.tsdk` if it is specified.
53-
const resolvedFromTsdk = resolveTsServerFromTsdk(probeLocations[0], probeLocations.slice(1));
50+
export function resolveTsServer(probeLocations: string[], tsdk: string|null): NodeModule {
51+
if (tsdk !== null) {
52+
const resolvedFromTsdk = resolveTsServerFromTsdk(tsdk, probeLocations);
5453
const minVersion = new Version(MIN_TS_VERSION);
5554
if (resolvedFromTsdk !== undefined) {
5655
if (resolvedFromTsdk.version.greaterThanOrEqual(minVersion)) {

0 commit comments

Comments
 (0)