fix(language-service): Add plugin option to force strictTemplates (#41062)

This commit adds a new configuration option, `forceStrictTemplates` to the
language service plugin to allow users to force enable `strictTemplates`.

This is needed so that the Angular extension can be used inside Google without
changing the underlying compiler options in the `ng_module` build rule.

PR Close #41062
This commit is contained in:
Keen Yee Liau 2021-03-02 15:46:11 -08:00 committed by Zach Arend
parent 21f0deeaa6
commit e9e7c33f3c
10 changed files with 67 additions and 17 deletions

View File

@ -14,7 +14,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
export interface NgLanguageServiceConfig { export interface PluginConfig {
/** /**
* If true, return only Angular results. Otherwise, return Angular + TypeScript * If true, return only Angular results. Otherwise, return Angular + TypeScript
* results. * results.
@ -25,6 +25,11 @@ export interface NgLanguageServiceConfig {
* Otherwise return factory function for View Engine LS. * Otherwise return factory function for View Engine LS.
*/ */
ivy: boolean; ivy: boolean;
/**
* If true, enable `strictTemplates` in Angular compiler options regardless
* of its value in tsconfig.json.
*/
forceStrictTemplates?: true;
} }
export type GetTcbResponse = { export type GetTcbResponse = {

View File

@ -7,13 +7,13 @@
*/ */
import * as ts from 'typescript/lib/tsserverlibrary'; import * as ts from 'typescript/lib/tsserverlibrary';
import {NgLanguageService, NgLanguageServiceConfig} from './api'; import {NgLanguageService, PluginConfig} from './api';
export * from './api'; export * from './api';
interface PluginModule extends ts.server.PluginModule { interface PluginModule extends ts.server.PluginModule {
create(createInfo: ts.server.PluginCreateInfo): NgLanguageService; create(createInfo: ts.server.PluginCreateInfo): NgLanguageService;
onConfigurationChanged?(config: NgLanguageServiceConfig): void; onConfigurationChanged?(config: PluginConfig): void;
} }
const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => { const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => {
@ -21,7 +21,7 @@ const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => {
return { return {
create(info: ts.server.PluginCreateInfo): NgLanguageService { create(info: ts.server.PluginCreateInfo): NgLanguageService {
const config: NgLanguageServiceConfig = info.config; const config: PluginConfig = info.config;
const bundleName = config.ivy ? 'ivy.js' : 'language-service.js'; const bundleName = config.ivy ? 'ivy.js' : 'language-service.js';
plugin = require(`./bundles/${bundleName}`)(tsModule); plugin = require(`./bundles/${bundleName}`)(tsModule);
return plugin.create(info); return plugin.create(info);
@ -29,7 +29,7 @@ const factory: ts.server.PluginModuleFactory = (tsModule): PluginModule => {
getExternalFiles(project: ts.server.Project): string[] { getExternalFiles(project: ts.server.Project): string[] {
return plugin?.getExternalFiles?.(project) ?? []; return plugin?.getExternalFiles?.(project) ?? [];
}, },
onConfigurationChanged(config: NgLanguageServiceConfig): void { onConfigurationChanged(config: PluginConfig): void {
plugin?.onConfigurationChanged?.(config); plugin?.onConfigurationChanged?.(config);
}, },
}; };

View File

@ -29,6 +29,14 @@ import {getTargetAtPosition, TargetContext, TargetNodeKind} from './template_tar
import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils'; import {findTightestNode, getClassDeclFromDecoratorProp, getPropertyAssignmentFromValue} from './ts_utils';
import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils'; import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils';
interface LanguageServiceConfig {
/**
* If true, enable `strictTemplates` in Angular compiler options regardless
* of its value in tsconfig.json.
*/
forceStrictTemplates?: true;
}
export class LanguageService { export class LanguageService {
private options: CompilerOptions; private options: CompilerOptions;
readonly compilerFactory: CompilerFactory; readonly compilerFactory: CompilerFactory;
@ -37,9 +45,12 @@ export class LanguageService {
private readonly parseConfigHost: LSParseConfigHost; private readonly parseConfigHost: LSParseConfigHost;
constructor( constructor(
private readonly project: ts.server.Project, private readonly tsLS: ts.LanguageService) { private readonly project: ts.server.Project,
private readonly tsLS: ts.LanguageService,
private readonly config: LanguageServiceConfig,
) {
this.parseConfigHost = new LSParseConfigHost(project.projectService.host); this.parseConfigHost = new LSParseConfigHost(project.projectService.host);
this.options = parseNgCompilerOptions(project, this.parseConfigHost); this.options = parseNgCompilerOptions(project, this.parseConfigHost, config);
logCompilerOptions(project, this.options); logCompilerOptions(project, this.options);
this.strategy = createTypeCheckingProgramStrategy(project); this.strategy = createTypeCheckingProgramStrategy(project);
this.adapter = new LanguageServiceAdapter(project); this.adapter = new LanguageServiceAdapter(project);
@ -340,7 +351,7 @@ export class LanguageService {
project.getConfigFilePath(), (fileName: string, eventKind: ts.FileWatcherEventKind) => { project.getConfigFilePath(), (fileName: string, eventKind: ts.FileWatcherEventKind) => {
project.log(`Config file changed: ${fileName}`); project.log(`Config file changed: ${fileName}`);
if (eventKind === ts.FileWatcherEventKind.Changed) { if (eventKind === ts.FileWatcherEventKind.Changed) {
this.options = parseNgCompilerOptions(project, this.parseConfigHost); this.options = parseNgCompilerOptions(project, this.parseConfigHost, this.config);
logCompilerOptions(project, this.options); logCompilerOptions(project, this.options);
} }
}); });
@ -354,7 +365,8 @@ function logCompilerOptions(project: ts.server.Project, options: CompilerOptions
} }
function parseNgCompilerOptions( function parseNgCompilerOptions(
project: ts.server.Project, host: ConfigurationHost): CompilerOptions { project: ts.server.Project, host: ConfigurationHost,
config: LanguageServiceConfig): CompilerOptions {
if (!(project instanceof ts.server.ConfiguredProject)) { if (!(project instanceof ts.server.ConfiguredProject)) {
return {}; return {};
} }
@ -375,6 +387,12 @@ function parseNgCompilerOptions(
// and only the real component declaration is used. // and only the real component declaration is used.
options.compileNonExportedClasses = false; options.compileNonExportedClasses = false;
// If `forceStrictTemplates` is true, always enable `strictTemplates`
// regardless of its value in tsconfig.json.
if (config.forceStrictTemplates === true) {
options.strictTemplates = true;
}
return options; return options;
} }

View File

@ -18,7 +18,7 @@ describe('definitions', () => {
beforeAll(() => { beforeAll(() => {
const {project, service: _service, tsLS} = setup(); const {project, service: _service, tsLS} = setup();
service = _service; service = _service;
ngLS = new LanguageService(project, tsLS); ngLS = new LanguageService(project, tsLS, {});
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -17,7 +17,7 @@ describe('getSemanticDiagnostics', () => {
beforeAll(() => { beforeAll(() => {
const {project, service: _service, tsLS} = setup(); const {project, service: _service, tsLS} = setup();
service = _service; service = _service;
ngLS = new LanguageService(project, tsLS); ngLS = new LanguageService(project, tsLS, {});
}); });
beforeEach(() => { beforeEach(() => {

View File

@ -23,7 +23,7 @@ describe('language service adapter', () => {
const {project: _project, tsLS, service: _service, configFileFs: _configFileFs} = setup(); const {project: _project, tsLS, service: _service, configFileFs: _configFileFs} = setup();
project = _project; project = _project;
service = _service; service = _service;
ngLS = new LanguageService(project, tsLS); ngLS = new LanguageService(project, tsLS, {});
configFileFs = _configFileFs; configFileFs = _configFileFs;
}); });
@ -57,6 +57,33 @@ describe('language service adapter', () => {
strictTemplates: false, strictTemplates: false,
})); }));
}); });
it('should always enable strictTemplates if forceStrictTemplates is true', () => {
const {project, tsLS, configFileFs} = setup();
const ngLS = new LanguageService(project, tsLS, {
forceStrictTemplates: true,
});
// First make sure the default for strictTemplates is true
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
enableIvy: true, // default for ivy is true
strictTemplates: true,
strictInjectionParameters: true,
}));
// Change strictTemplates to false
configFileFs.overwriteConfigFile(TSCONFIG, {
angularCompilerOptions: {
strictTemplates: false,
}
});
// Make sure strictTemplates is still true because forceStrictTemplates
// is enabled.
expect(ngLS.getCompilerOptions()).toEqual(jasmine.objectContaining({
strictTemplates: true,
}));
});
}); });
describe('compiler options diagnostics', () => { describe('compiler options diagnostics', () => {

View File

@ -19,7 +19,7 @@ describe('getExternalFiles()', () => {
// a global analysis // a global analysis
expect(externalFiles).toEqual([]); expect(externalFiles).toEqual([]);
// Trigger global analysis // Trigger global analysis
const ngLS = new LanguageService(project, tsLS); const ngLS = new LanguageService(project, tsLS, {});
ngLS.getSemanticDiagnostics(APP_COMPONENT); ngLS.getSemanticDiagnostics(APP_COMPONENT);
// Now that global analysis is run, we should have all the typecheck files // Now that global analysis is run, we should have all the typecheck files
externalFiles = getExternalFiles(project); externalFiles = getExternalFiles(project);

View File

@ -18,7 +18,7 @@ describe('type definitions', () => {
beforeAll(() => { beforeAll(() => {
const {project, service: _service, tsLS} = setup(); const {project, service: _service, tsLS} = setup();
service = _service; service = _service;
ngLS = new LanguageService(project, tsLS); ngLS = new LanguageService(project, tsLS, {});
}); });
const possibleArrayDefFiles = new Set([ const possibleArrayDefFiles = new Set([

View File

@ -92,7 +92,7 @@ export class Project {
// The following operation forces a ts.Program to be created. // The following operation forces a ts.Program to be created.
this.tsLS = tsProject.getLanguageService(); this.tsLS = tsProject.getLanguageService();
this.ngLS = new LanguageService(tsProject, this.tsLS); this.ngLS = new LanguageService(tsProject, this.tsLS, {});
} }
openFile(projectFileName: string): OpenBuffer { openFile(projectFileName: string): OpenBuffer {
@ -188,4 +188,4 @@ function getClassOrError(sf: ts.SourceFile, name: string): ts.ClassDeclaration {
} }
} }
throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`); throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`);
} }

View File

@ -16,7 +16,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
const {project, languageService: tsLS, config} = info; const {project, languageService: tsLS, config} = info;
const angularOnly = config?.angularOnly === true; const angularOnly = config?.angularOnly === true;
const ngLS = new LanguageService(project, tsLS); const ngLS = new LanguageService(project, tsLS, config);
function getSemanticDiagnostics(fileName: string): ts.Diagnostic[] { function getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = []; const diagnostics: ts.Diagnostic[] = [];