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:
parent
21f0deeaa6
commit
e9e7c33f3c
|
@ -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 = {
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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(() => {
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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([
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[] = [];
|
||||||
|
|
Loading…
Reference in New Issue