2018-09-25 18:35:03 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2019-09-07 17:48:32 -04:00
|
|
|
import {CustomTransformers, Program, defaultGatherDiagnostics} from '@angular/compiler-cli';
|
2019-04-27 18:26:13 -04:00
|
|
|
import * as api from '@angular/compiler-cli/src/transformers/api';
|
2018-09-25 18:35:03 -04:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2019-10-17 11:36:04 -04:00
|
|
|
import {createCompilerHost, createProgram} from '../../index';
|
2018-11-16 11:54:43 -05:00
|
|
|
import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main';
|
2019-06-06 15:22:32 -04:00
|
|
|
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost, absoluteFrom, getFileSystem} from '../../src/ngtsc/file_system';
|
|
|
|
import {Folder, MockFileSystem} from '../../src/ngtsc/file_system/testing';
|
|
|
|
import {IndexedComponent} from '../../src/ngtsc/indexer';
|
|
|
|
import {NgtscProgram} from '../../src/ngtsc/program';
|
2018-11-16 11:54:43 -05:00
|
|
|
import {LazyRoute} from '../../src/ngtsc/routing';
|
2019-06-06 15:22:32 -04:00
|
|
|
import {setWrapHostForTest} from '../../src/transformers/compiler_host';
|
2018-09-25 18:35:03 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Manages a temporary testing directory structure and environment for testing ngtsc by feeding it
|
|
|
|
* TypeScript code.
|
|
|
|
*/
|
|
|
|
export class NgtscTestEnvironment {
|
2019-03-18 14:21:29 -04:00
|
|
|
private multiCompileHostExt: MultiCompileHostExt|null = null;
|
|
|
|
private oldProgram: Program|null = null;
|
2019-06-27 19:25:00 -04:00
|
|
|
private changedResources: Set<string>|null = null;
|
2019-03-18 14:21:29 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
private constructor(
|
|
|
|
private fs: FileSystem, readonly outDir: AbsoluteFsPath, readonly basePath: AbsoluteFsPath) {}
|
2018-09-25 18:35:03 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up a new testing environment.
|
|
|
|
*/
|
2019-11-13 17:45:46 -05:00
|
|
|
static setup(files?: Folder, workingDir: AbsoluteFsPath = absoluteFrom('/')):
|
|
|
|
NgtscTestEnvironment {
|
2019-06-06 15:22:32 -04:00
|
|
|
const fs = getFileSystem();
|
|
|
|
if (files !== undefined && fs instanceof MockFileSystem) {
|
|
|
|
fs.init(files);
|
|
|
|
}
|
2018-09-25 18:35:03 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
const host = new AugmentedCompilerHost(fs);
|
|
|
|
setWrapHostForTest(makeWrapHost(host));
|
2018-09-25 18:35:03 -04:00
|
|
|
|
2019-11-13 17:45:46 -05:00
|
|
|
const env = new NgtscTestEnvironment(fs, fs.resolve('/built'), workingDir);
|
|
|
|
fs.chdir(workingDir);
|
2018-09-25 18:35:03 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write(absoluteFrom('/tsconfig-base.json'), `{
|
2018-09-25 18:35:03 -04:00
|
|
|
"compilerOptions": {
|
2019-02-22 21:06:25 -05:00
|
|
|
"emitDecoratorMetadata": true,
|
2018-09-25 18:35:03 -04:00
|
|
|
"experimentalDecorators": true,
|
|
|
|
"skipLibCheck": true,
|
|
|
|
"noImplicitAny": true,
|
2019-11-12 16:38:23 -05:00
|
|
|
"noEmitOnError": true,
|
2018-09-25 18:35:03 -04:00
|
|
|
"strictNullChecks": true,
|
|
|
|
"outDir": "built",
|
|
|
|
"baseUrl": ".",
|
|
|
|
"declaration": true,
|
|
|
|
"target": "es5",
|
2019-01-25 13:49:08 -05:00
|
|
|
"newLine": "lf",
|
2018-09-25 18:35:03 -04:00
|
|
|
"module": "es2015",
|
|
|
|
"moduleResolution": "node",
|
|
|
|
"lib": ["es6", "dom"],
|
|
|
|
"typeRoots": ["node_modules/@types"]
|
|
|
|
},
|
|
|
|
"angularCompilerOptions": {
|
2019-04-02 14:52:19 -04:00
|
|
|
"enableIvy": true,
|
|
|
|
"ivyTemplateTypeCheck": false
|
2019-03-18 14:21:29 -04:00
|
|
|
},
|
|
|
|
"exclude": [
|
|
|
|
"built"
|
|
|
|
]
|
2018-09-25 18:35:03 -04:00
|
|
|
}`);
|
|
|
|
|
|
|
|
return env;
|
|
|
|
}
|
|
|
|
|
|
|
|
assertExists(fileName: string) {
|
2019-06-06 15:22:32 -04:00
|
|
|
if (!this.fs.exists(this.fs.resolve(this.outDir, fileName))) {
|
2018-09-25 18:35:03 -04:00
|
|
|
throw new Error(`Expected ${fileName} to be emitted (outDir: ${this.outDir})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
assertDoesNotExist(fileName: string) {
|
2019-06-06 15:22:32 -04:00
|
|
|
if (this.fs.exists(this.fs.resolve(this.outDir, fileName))) {
|
2018-09-25 18:35:03 -04:00
|
|
|
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${this.outDir})`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getContents(fileName: string): string {
|
|
|
|
this.assertExists(fileName);
|
2019-06-06 15:22:32 -04:00
|
|
|
const modulePath = this.fs.resolve(this.outDir, fileName);
|
|
|
|
return this.fs.readFile(modulePath);
|
2018-09-25 18:35:03 -04:00
|
|
|
}
|
|
|
|
|
2019-03-18 14:21:29 -04:00
|
|
|
enableMultipleCompilations(): void {
|
2019-06-10 11:22:56 -04:00
|
|
|
this.changedResources = new Set();
|
2019-06-06 15:22:32 -04:00
|
|
|
this.multiCompileHostExt = new MultiCompileHostExt(this.fs);
|
2019-03-18 14:21:29 -04:00
|
|
|
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
|
|
|
|
}
|
|
|
|
|
2019-09-07 17:48:32 -04:00
|
|
|
/**
|
|
|
|
* Installs a compiler host that allows for asynchronous reading of resources by implementing the
|
|
|
|
* `CompilerHost.readResource` method. Note that only asynchronous compilations are affected, as
|
|
|
|
* synchronous compilations do not use the asynchronous resource loader.
|
|
|
|
*/
|
|
|
|
enablePreloading(): void {
|
|
|
|
setWrapHostForTest(makeWrapHost(new ResourceLoadingCompileHost(this.fs)));
|
|
|
|
}
|
|
|
|
|
2019-03-18 14:21:29 -04:00
|
|
|
flushWrittenFileTracking(): void {
|
|
|
|
if (this.multiCompileHostExt === null) {
|
|
|
|
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
|
|
|
}
|
2019-06-10 11:22:56 -04:00
|
|
|
this.changedResources !.clear();
|
2019-03-18 14:21:29 -04:00
|
|
|
this.multiCompileHostExt.flushWrittenFileTracking();
|
|
|
|
}
|
|
|
|
|
2019-06-27 19:25:00 -04:00
|
|
|
/**
|
|
|
|
* Older versions of the CLI do not provide the `CompilerHost.getModifiedResourceFiles()` method.
|
|
|
|
* This results in the `changedResources` set being `null`.
|
|
|
|
*/
|
|
|
|
simulateLegacyCLICompilerHost() { this.changedResources = null; }
|
|
|
|
|
2019-03-18 14:21:29 -04:00
|
|
|
getFilesWrittenSinceLastFlush(): Set<string> {
|
|
|
|
if (this.multiCompileHostExt === null) {
|
|
|
|
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
|
|
|
|
}
|
|
|
|
const writtenFiles = new Set<string>();
|
|
|
|
this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => {
|
2019-06-06 15:22:32 -04:00
|
|
|
if (rawFile.startsWith(this.outDir)) {
|
|
|
|
writtenFiles.add(rawFile.substr(this.outDir.length));
|
2019-03-18 14:21:29 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return writtenFiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
write(fileName: string, content: string) {
|
2019-06-06 15:22:32 -04:00
|
|
|
const absFilePath = this.fs.resolve(this.basePath, fileName);
|
2019-03-18 14:21:29 -04:00
|
|
|
if (this.multiCompileHostExt !== null) {
|
|
|
|
this.multiCompileHostExt.invalidate(absFilePath);
|
2019-06-10 11:22:56 -04:00
|
|
|
this.changedResources !.add(absFilePath);
|
2019-03-18 14:21:29 -04:00
|
|
|
}
|
2019-06-06 15:22:32 -04:00
|
|
|
this.fs.ensureDir(this.fs.dirname(absFilePath));
|
|
|
|
this.fs.writeFile(absFilePath, content);
|
2019-03-18 14:21:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
invalidateCachedFile(fileName: string): void {
|
2019-06-06 15:22:32 -04:00
|
|
|
const absFilePath = this.fs.resolve(this.basePath, fileName);
|
2019-03-18 14:21:29 -04:00
|
|
|
if (this.multiCompileHostExt === null) {
|
|
|
|
throw new Error(`Not caching files - call enableMultipleCompilations()`);
|
|
|
|
}
|
2019-06-06 15:22:32 -04:00
|
|
|
this.multiCompileHostExt.invalidate(absFilePath);
|
2019-03-18 14:21:29 -04:00
|
|
|
}
|
2018-09-25 18:35:03 -04:00
|
|
|
|
2019-08-21 14:51:24 -04:00
|
|
|
tsconfig(extraOpts: {[key: string]: string | boolean | null} = {}, extraRootDirs?: string[]):
|
|
|
|
void {
|
2018-11-30 13:37:06 -05:00
|
|
|
const tsconfig: {[key: string]: any} = {
|
|
|
|
extends: './tsconfig-base.json',
|
2019-02-08 06:37:21 -05:00
|
|
|
angularCompilerOptions: {...extraOpts, enableIvy: true},
|
2018-11-30 13:37:06 -05:00
|
|
|
};
|
|
|
|
if (extraRootDirs !== undefined) {
|
|
|
|
tsconfig.compilerOptions = {
|
|
|
|
rootDirs: ['.', ...extraRootDirs],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
|
2019-02-19 20:36:26 -05:00
|
|
|
|
|
|
|
if (extraOpts['_useHostForImportGeneration'] === true) {
|
2019-06-06 15:22:32 -04:00
|
|
|
setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost(this.fs)));
|
2019-02-19 20:36:26 -05:00
|
|
|
}
|
2018-09-25 18:35:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run the compiler to completion, and assert that no errors occurred.
|
|
|
|
*/
|
2019-01-03 05:23:00 -05:00
|
|
|
driveMain(customTransformers?: CustomTransformers): void {
|
2018-09-25 18:35:03 -04:00
|
|
|
const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
2019-03-18 14:21:29 -04:00
|
|
|
let reuseProgram: {program: Program | undefined}|undefined = undefined;
|
|
|
|
if (this.multiCompileHostExt !== null) {
|
|
|
|
reuseProgram = {
|
|
|
|
program: this.oldProgram || undefined,
|
|
|
|
};
|
|
|
|
}
|
2019-06-10 11:22:56 -04:00
|
|
|
const exitCode = main(
|
|
|
|
['-p', this.basePath], errorSpy, undefined, customTransformers, reuseProgram,
|
|
|
|
this.changedResources);
|
2018-09-25 18:35:03 -04:00
|
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
|
|
expect(exitCode).toBe(0);
|
2019-03-18 14:21:29 -04:00
|
|
|
if (this.multiCompileHostExt !== null) {
|
|
|
|
this.oldProgram = reuseProgram !.program !;
|
|
|
|
}
|
2018-09-25 18:35:03 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run the compiler to completion, and return any `ts.Diagnostic` errors that may have occurred.
|
|
|
|
*/
|
feat(ivy): convert all ngtsc diagnostics to ts.Diagnostics (#31952)
Historically, the Angular Compiler has produced both native TypeScript
diagnostics (called ts.Diagnostics) and its own internal Diagnostic format
(called an api.Diagnostic). This was done because TypeScript ts.Diagnostics
cannot be produced for files not in the ts.Program, and template type-
checking diagnostics are naturally produced for external .html template
files.
This design isn't optimal for several reasons:
1) Downstream tooling (such as the CLI) must support multiple formats of
diagnostics, adding to the maintenance burden.
2) ts.Diagnostics have gotten a lot better in recent releases, with support
for suggested changes, highlighting of the code in question, etc. None of
these changes have been of any benefit for api.Diagnostics, which have
continued to be reported in a very primitive fashion.
3) A future plugin model will not support anything but ts.Diagnostics, so
generating api.Diagnostics is a blocker for ngtsc-as-a-plugin.
4) The split complicates both the typings and the testing of ngtsc.
To fix this issue, this commit changes template type-checking to produce
ts.Diagnostics instead. Instead of reporting a special kind of diagnostic
for external template files, errors in a template are always reported in
a ts.Diagnostic that highlights the portion of the template which contains
the error. When this template text is distinct from the source .ts file
(for example, when the template is parsed from an external resource file),
additional contextual information links the error back to the originating
component.
A template error can thus be reported in 3 separate ways, depending on how
the template was configured:
1) For inline template strings which can be directly mapped to offsets in
the TS code, ts.Diagnostics point to real ranges in the source.
This is the case if an inline template is used with a string literal or a
"no-substitution" string. For example:
```typescript
@Component({..., template: `
<p>Bar: {{baz}}</p>
`})
export class TestCmp {
bar: string;
}
```
The above template contains an error (no 'baz' property of `TestCmp`). The
error produced by TS will look like:
```
<p>Bar: {{baz}}</p>
~~~
test.ts:2:11 - error TS2339: Property 'baz' does not exist on type 'TestCmp'. Did you mean 'bar'?
```
2) For template strings which cannot be directly mapped to offsets in the
TS code, a logical offset into the template string will be included in
the error message. For example:
```typescript
const SOME_TEMPLATE = '<p>Bar: {{baz}}</p>';
@Component({..., template: SOME_TEMPLATE})
export class TestCmp {
bar: string;
}
```
Because the template is a reference to another variable and is not an
inline string constant, the compiler will not be able to use "absolute"
positions when parsing the template. As a result, errors will report logical
offsets into the template string:
```
<p>Bar: {{baz}}</p>
~~~
test.ts (TestCmp template):2:15 - error TS2339: Property 'baz' does not exist on type 'TestCmp'.
test.ts:3:28
@Component({..., template: TEMPLATE})
~~~~~~~~
Error occurs in the template of component TestCmp.
```
This error message uses logical offsets into the template string, and also
gives a reference to the `TEMPLATE` expression from which the template was
parsed. This helps in locating the component which contains the error.
3) For external templates (templateUrl), the error message is delivered
within the HTML template file (testcmp.html) instead, and additional
information contextualizes the error on the templateUrl expression from
which the template file was determined:
```
<p>Bar: {{baz}}</p>
~~~
testcmp.html:2:15 - error TS2339: Property 'baz' does not exist on type 'TestCmp'.
test.ts:10:31
@Component({..., templateUrl: './testcmp.html'})
~~~~~~~~~~~~~~~~
Error occurs in the template of component TestCmp.
```
PR Close #31952
2019-08-01 18:01:55 -04:00
|
|
|
driveDiagnostics(): ReadonlyArray<ts.Diagnostic> {
|
|
|
|
// ngtsc only produces ts.Diagnostic messages.
|
2019-11-12 16:38:23 -05:00
|
|
|
let reuseProgram: {program: Program | undefined}|undefined = undefined;
|
|
|
|
if (this.multiCompileHostExt !== null) {
|
|
|
|
reuseProgram = {
|
|
|
|
program: this.oldProgram || undefined,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const diags = mainDiagnosticsForTest(
|
|
|
|
['-p', this.basePath], undefined, reuseProgram, this.changedResources);
|
|
|
|
|
|
|
|
|
|
|
|
if (this.multiCompileHostExt !== null) {
|
|
|
|
this.oldProgram = reuseProgram !.program !;
|
|
|
|
}
|
|
|
|
|
|
|
|
// In ngtsc, only `ts.Diagnostic`s are produced.
|
|
|
|
return diags as ReadonlyArray<ts.Diagnostic>;
|
2018-09-25 18:35:03 -04:00
|
|
|
}
|
|
|
|
|
2019-09-07 17:48:32 -04:00
|
|
|
async driveDiagnosticsAsync(): Promise<ReadonlyArray<ts.Diagnostic>> {
|
|
|
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
|
|
|
const host = createCompilerHost({options});
|
|
|
|
const program = createProgram({rootNames, host, options});
|
|
|
|
await program.loadNgStructureAsync();
|
|
|
|
|
|
|
|
// ngtsc only produces ts.Diagnostic messages.
|
|
|
|
return defaultGatherDiagnostics(program as api.Program) as ts.Diagnostic[];
|
|
|
|
}
|
|
|
|
|
2018-11-16 11:54:43 -05:00
|
|
|
driveRoutes(entryPoint?: string): LazyRoute[] {
|
|
|
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
|
|
|
const host = createCompilerHost({options});
|
|
|
|
const program = createProgram({rootNames, host, options});
|
|
|
|
return program.listLazyRoutes(entryPoint);
|
|
|
|
}
|
2019-06-19 20:23:59 -04:00
|
|
|
|
|
|
|
driveIndexer(): Map<ts.Declaration, IndexedComponent> {
|
|
|
|
const {rootNames, options} = readNgcCommandLineAndConfiguration(['-p', this.basePath]);
|
|
|
|
const host = createCompilerHost({options});
|
|
|
|
const program = createProgram({rootNames, host, options});
|
|
|
|
return (program as NgtscProgram).getIndexedComponents();
|
|
|
|
}
|
2018-09-25 18:35:03 -04:00
|
|
|
}
|
2019-03-18 14:21:29 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
class AugmentedCompilerHost extends NgtscCompilerHost {
|
2019-03-18 14:21:29 -04:00
|
|
|
delegate !: ts.CompilerHost;
|
|
|
|
}
|
|
|
|
|
|
|
|
class FileNameToModuleNameHost extends AugmentedCompilerHost {
|
|
|
|
fileNameToModuleName(importedFilePath: string): string {
|
2019-06-06 15:22:32 -04:00
|
|
|
const relativeFilePath = this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath));
|
|
|
|
const rootedPath = this.fs.join('root', relativeFilePath);
|
|
|
|
return rootedPath.replace(/(\.d)?.ts$/, '');
|
2019-03-18 14:21:29 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.CompilerHost> {
|
|
|
|
private cache = new Map<string, ts.SourceFile>();
|
|
|
|
private writtenFiles = new Set<string>();
|
|
|
|
|
|
|
|
getSourceFile(
|
|
|
|
fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void,
|
|
|
|
shouldCreateNewSourceFile?: boolean): ts.SourceFile|undefined {
|
|
|
|
if (this.cache.has(fileName)) {
|
|
|
|
return this.cache.get(fileName) !;
|
|
|
|
}
|
2019-06-06 15:22:32 -04:00
|
|
|
const sf = super.getSourceFile(fileName, languageVersion);
|
2019-03-18 14:21:29 -04:00
|
|
|
if (sf !== undefined) {
|
|
|
|
this.cache.set(sf.fileName, sf);
|
|
|
|
}
|
|
|
|
return sf;
|
|
|
|
}
|
|
|
|
|
|
|
|
flushWrittenFileTracking(): void { this.writtenFiles.clear(); }
|
|
|
|
|
|
|
|
writeFile(
|
|
|
|
fileName: string, data: string, writeByteOrderMark: boolean,
|
|
|
|
onError: ((message: string) => void)|undefined,
|
|
|
|
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
|
2019-06-06 15:22:32 -04:00
|
|
|
super.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
2019-03-18 14:21:29 -04:00
|
|
|
this.writtenFiles.add(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
getFilesWrittenSinceLastFlush(): Set<string> { return this.writtenFiles; }
|
|
|
|
|
|
|
|
invalidate(fileName: string): void { this.cache.delete(fileName); }
|
|
|
|
}
|
|
|
|
|
2019-09-07 17:48:32 -04:00
|
|
|
class ResourceLoadingCompileHost extends AugmentedCompilerHost implements api.CompilerHost {
|
|
|
|
readResource(fileName: string): Promise<string>|string {
|
|
|
|
const resource = this.readFile(fileName);
|
|
|
|
if (resource === undefined) {
|
|
|
|
throw new Error(`Resource ${fileName} not found`);
|
|
|
|
}
|
|
|
|
return resource;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-18 14:21:29 -04:00
|
|
|
function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) => ts.CompilerHost {
|
|
|
|
return (delegate) => {
|
|
|
|
wrapped.delegate = delegate;
|
|
|
|
return new Proxy(delegate, {
|
|
|
|
get: (target: ts.CompilerHost, name: string): any => {
|
|
|
|
if ((wrapped as any)[name] !== undefined) {
|
|
|
|
return (wrapped as any)[name] !.bind(wrapped);
|
|
|
|
}
|
|
|
|
return (target as any)[name];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|