refactor(ivy): implement a virtual file-system layer in ngtsc + ngcc (#30921)

To improve cross platform support, all file access (and path manipulation)
is now done through a well known interface (`FileSystem`).

For testing a number of `MockFileSystem` implementations are provided.
These provide an in-memory file-system which emulates operating systems
like OS/X, Unix and Windows.

The current file system is always available via the static method,
`FileSystem.getFileSystem()`. This is also used by a number of static
methods on `AbsoluteFsPath` and `PathSegment`, to avoid having to pass
`FileSystem` objects around all the time. The result of this is that one
must be careful to ensure that the file-system has been initialized before
using any of these static methods. To prevent this happening accidentally
the current file system always starts out as an instance of `InvalidFileSystem`,
which will throw an error if any of its methods are called.

You can set the current file-system by calling `FileSystem.setFileSystem()`.
During testing you can call the helper function `initMockFileSystem(os)`
which takes a string name of the OS to emulate, and will also monkey-patch
aspects of the TypeScript library to ensure that TS is also using the
current file-system.

Finally there is the `NgtscCompilerHost` to be used for any TypeScript
compilation, which uses a given file-system.

All tests that interact with the file-system should be tested against each
of the mock file-systems. A series of helpers have been provided to support
such tests:

* `runInEachFileSystem()` - wrap your tests in this helper to run all the
wrapped tests in each of the mock file-systems.
* `addTestFilesToFileSystem()` - use this to add files and their contents
to the mock file system for testing.
* `loadTestFilesFromDisk()` - use this to load a mirror image of files on
disk into the in-memory mock file-system.
* `loadFakeCore()` - use this to load a fake version of `@angular/core`
into the mock file-system.

All ngcc and ngtsc source and tests now use this virtual file-system setup.

PR Close #30921
This commit is contained in:
Pete Bacon Darwin
2019-06-06 20:22:32 +01:00
committed by Kara Erickson
parent 1e7e065423
commit 7186f9c016
177 changed files with 16598 additions and 14829 deletions

View File

@ -7,18 +7,18 @@ ts_library(
testonly = True,
srcs = [
"mocks.ts",
"runfile_helpers.ts",
"test_support.ts",
],
visibility = [
":__subpackages__",
"//packages/compiler-cli/ngcc/test:__subpackages__",
"//packages/language-service/test:__subpackages__",
],
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/test/helpers",
"@npm//typescript",
],
)

View File

@ -10,6 +10,7 @@ ts_library(
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler/test:test_utils",
"@npm//typescript",
],

View File

@ -5,15 +5,13 @@
* 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
*/
import {AotCompilerOptions} from '@angular/compiler';
import {escapeRegExp} from '@angular/compiler/src/util';
import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, toMockFileArray} from '@angular/compiler/test/aot/test_util';
import {MockCompilerHost, MockData, MockDirectory, arrayToMockDir, toMockFileArray} from '@angular/compiler/test/aot/test_util';
import * as ts from 'typescript';
import {NodeJSFileSystem, setFileSystem} from '../../src/ngtsc/file_system';
import {NgtscProgram} from '../../src/ngtsc/program';
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
const OPERATOR =
/!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\./;
@ -169,6 +167,7 @@ export function compile(
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}): {
source: string,
} {
setFileSystem(new NodeJSFileSystem());
const testFiles = toMockFileArray(data);
const scripts = testFiles.map(entry => entry.fileName);
const angularFilesArray = toMockFileArray(angularFiles);

View File

@ -5,13 +5,11 @@
* 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
*/
/// <reference types="node" />
import * as ng from '@angular/compiler-cli';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as ts from 'typescript';
import {TestSupport, expectNoDiagnostics, setup} from '../test_support';
type MockFiles = {

View File

@ -114,8 +114,6 @@ export class DiagnosticContext {
_reflector: StaticReflector|undefined;
_errors: {e: any, path?: string}[] = [];
_resolver: CompileMetadataResolver|undefined;
// TODO(issue/24571): remove '!'.
_refletor !: StaticReflector;
// tslint:enable
constructor(

View File

@ -10,7 +10,7 @@ import * as fs from 'fs';
import * as path from 'path';
import {mainXi18n} from '../src/extract_i18n';
import {makeTempDir, setup} from './test_support';
import {setup} from './test_support';
const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE messagebundle [

View File

@ -0,0 +1,17 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "helpers",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//typescript",
],
)

View File

@ -0,0 +1,9 @@
/**
* @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
*/
export {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from './src/runfile_helpers';
export * from './src/mock_file_loading';

View File

@ -0,0 +1,82 @@
/**
* @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
*/
/// <reference types="node" />
import {readFileSync, readdirSync, statSync} from 'fs';
import {resolve} from 'path';
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '..';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {Folder, MockFileSystemPosix, TestFile} from '../../../src/ngtsc/file_system/testing';
export function loadTestFiles(files: TestFile[]) {
const fs = getFileSystem();
files.forEach(file => {
fs.ensureDir(fs.dirname(file.name));
fs.writeFile(file.name, file.contents);
});
}
export function loadStandardTestFiles(
{fakeCore = true, rxjs = false}: {fakeCore?: boolean, rxjs?: boolean} = {}): Folder {
const tmpFs = new MockFileSystemPosix(true);
const basePath = '/' as AbsoluteFsPath;
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('typescript'),
tmpFs.resolve(basePath, 'node_modules/typescript'));
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('tslib'), tmpFs.resolve(basePath, 'node_modules/tslib'));
if (fakeCore) {
loadFakeCore(tmpFs, basePath);
} else {
getAngularPackagesFromRunfiles().forEach(({name, pkgPath}) => {
loadTestDirectory(tmpFs, pkgPath, tmpFs.resolve('/node_modules/@angular', name));
});
}
if (rxjs) {
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('rxjs'), tmpFs.resolve(basePath, 'node_modules/rxjs'));
}
return tmpFs.dump();
}
export function loadFakeCore(fs: FileSystem, basePath: string = '/') {
loadTestDirectory(
fs, resolveNpmTreeArtifact('angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package'),
fs.resolve(basePath, 'node_modules/@angular/core'));
}
/**
* Load real files from the real file-system into a mock file-system.
* @param fs the file-system where the directory is to be loaded.
* @param directoryPath the path to the directory we want to load.
* @param mockPath the path within the mock file-system where the directory is to be loaded.
*/
function loadTestDirectory(fs: FileSystem, directoryPath: string, mockPath: AbsoluteFsPath): void {
readdirSync(directoryPath).forEach(item => {
const srcPath = resolve(directoryPath, item);
const targetPath = fs.resolve(mockPath, item);
try {
if (statSync(srcPath).isDirectory()) {
fs.ensureDir(targetPath);
loadTestDirectory(fs, srcPath, targetPath);
} else {
fs.ensureDir(fs.dirname(targetPath));
fs.writeFile(targetPath, readFileSync(srcPath, 'utf-8'));
}
} catch (e) {
console.warn(`Failed to add ${srcPath} to the mock file-system: ${e.message}`);
}
});
}

View File

@ -5,6 +5,7 @@
* 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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as path from 'path';

View File

@ -5,8 +5,6 @@
* 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
*/
import * as fs from 'fs';
import * as ts from 'typescript';
import {Evaluator} from '../../src/metadata/evaluator';

View File

@ -2028,7 +2028,7 @@ describe('ngc transformer command-line', () => {
const exitCode =
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
expect(exitCode).toBe(1, 'Compile was expected to fail');
const srcPathWithSep = `lib${path.sep}`;
const srcPathWithSep = `lib/`;
expect(messages[0])
.toEqual(
`${srcPathWithSep}test.component.ts(6,21): Error during template compile of 'TestComponent'

View File

@ -8,11 +8,13 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/test:test_utils",
"//packages/compiler-cli/test/helpers",
"@npm//@types/source-map",
"@npm//source-map",
"@npm//typescript",
@ -21,6 +23,7 @@ ts_library(
jasmine_node_test(
name = "ngtsc",
timeout = "long",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
data = [
"//packages/compiler-cli/test/ngtsc/fake_core:npm_package",

View File

@ -5,25 +5,29 @@
* 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
*/
import {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system';
import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import {AbsoluteSourceSpan, IdentifierKind} from '@angular/compiler-cli/src/ngtsc/indexer';
import {ParseSourceFile} from '@angular/compiler/src/compiler';
import * as path from 'path';
import {NgtscTestEnvironment} from './env';
describe('ngtsc component indexing', () => {
let env !: NgtscTestEnvironment;
runInEachFileSystem(() => {
describe('ngtsc component indexing', () => {
let env !: NgtscTestEnvironment;
let testSourceFile: AbsoluteFsPath;
let testTemplateFile: AbsoluteFsPath;
function testPath(testFile: string): string { return path.posix.join(env.basePath, testFile); }
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.tsconfig();
testSourceFile = resolve(env.basePath, 'test.ts');
testTemplateFile = resolve(env.basePath, 'test.html');
});
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.tsconfig();
});
describe('indexing metadata', () => {
it('should generate component metadata', () => {
const componentContent = `
describe('indexing metadata', () => {
it('should generate component metadata', () => {
const componentContent = `
import {Component} from '@angular/core';
@Component({
@ -32,22 +36,22 @@ describe('ngtsc component indexing', () => {
})
export class TestCmp {}
`;
env.write('test.ts', componentContent);
const indexed = env.driveIndexer();
expect(indexed.size).toBe(1);
env.write(testSourceFile, componentContent);
const indexed = env.driveIndexer();
expect(indexed.size).toBe(1);
const [[decl, indexedComp]] = Array.from(indexed.entries());
const [[decl, indexedComp]] = Array.from(indexed.entries());
expect(decl.getText()).toContain('export class TestCmp {}');
expect(indexedComp).toEqual(jasmine.objectContaining({
name: 'TestCmp',
selector: 'test-cmp',
file: new ParseSourceFile(componentContent, testPath('test.ts')),
}));
});
expect(decl.getText()).toContain('export class TestCmp {}');
expect(indexedComp).toEqual(jasmine.objectContaining({
name: 'TestCmp',
selector: 'test-cmp',
file: new ParseSourceFile(componentContent, testSourceFile),
}));
});
it('should index inline templates', () => {
const componentContent = `
it('should index inline templates', () => {
const componentContent = `
import {Component} from '@angular/core';
@Component({
@ -56,25 +60,25 @@ describe('ngtsc component indexing', () => {
})
export class TestCmp { foo = 0; }
`;
env.write('test.ts', componentContent);
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
env.write(testSourceFile, componentContent);
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(127, 130),
}]),
usedComponents: new Set(),
isInline: true,
file: new ParseSourceFile(componentContent, testPath('test.ts')),
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(127, 130),
}]),
usedComponents: new Set(),
isInline: true,
file: new ParseSourceFile(componentContent, testSourceFile),
});
});
});
it('should index external templates', () => {
env.write('test.ts', `
it('should index external templates', () => {
env.write(testSourceFile, `
import {Component} from '@angular/core';
@Component({
@ -83,29 +87,29 @@ describe('ngtsc component indexing', () => {
})
export class TestCmp { foo = 0; }
`);
env.write('test.html', '{{foo}}');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
env.write(testTemplateFile, '{{foo}}');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('{{foo}}', testPath('test.html')),
});
});
it('should index templates compiled without preserving whitespace', () => {
env.tsconfig({
preserveWhitespaces: false,
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(2, 5),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('{{foo}}', testTemplateFile),
});
});
env.write('test.ts', `
it('should index templates compiled without preserving whitespace', () => {
env.tsconfig({
preserveWhitespaces: false,
});
env.write(testSourceFile, `
import {Component} from '@angular/core';
@Component({
@ -114,25 +118,25 @@ describe('ngtsc component indexing', () => {
})
export class TestCmp { foo = 0; }
`);
env.write('test.html', '<div> \n {{foo}}</div>');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
env.write(testTemplateFile, '<div> \n {{foo}}</div>');
const indexed = env.driveIndexer();
const [[_, indexedComp]] = Array.from(indexed.entries());
const template = indexedComp.template;
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div> \n {{foo}}</div>', testPath('test.html')),
expect(template).toEqual({
identifiers: new Set([{
name: 'foo',
kind: IdentifierKind.Property,
span: new AbsoluteSourceSpan(12, 15),
}]),
usedComponents: new Set(),
isInline: false,
file: new ParseSourceFile('<div> \n {{foo}}</div>', testTemplateFile),
});
});
});
it('should generated information about used components', () => {
env.write('test.ts', `
it('should generate information about used components', () => {
env.write(testSourceFile, `
import {Component} from '@angular/core';
@Component({
@ -141,8 +145,8 @@ describe('ngtsc component indexing', () => {
})
export class TestCmp {}
`);
env.write('test.html', '<div></div>');
env.write('test_import.ts', `
env.write(testTemplateFile, '<div></div>');
env.write('test_import.ts', `
import {Component, NgModule} from '@angular/core';
import {TestCmp} from './test';
@ -160,21 +164,22 @@ describe('ngtsc component indexing', () => {
})
export class TestModule {}
`);
env.write('test_import.html', '<test-cmp></test-cmp>');
const indexed = env.driveIndexer();
expect(indexed.size).toBe(2);
env.write('test_import.html', '<test-cmp></test-cmp>');
const indexed = env.driveIndexer();
expect(indexed.size).toBe(2);
const indexedComps = Array.from(indexed.values());
const testComp = indexedComps.find(comp => comp.name === 'TestCmp');
const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp');
expect(testComp).toBeDefined();
expect(testImportComp).toBeDefined();
const indexedComps = Array.from(indexed.values());
const testComp = indexedComps.find(comp => comp.name === 'TestCmp');
const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp');
expect(testComp).toBeDefined();
expect(testImportComp).toBeDefined();
expect(testComp !.template.usedComponents.size).toBe(0);
expect(testImportComp !.template.usedComponents.size).toBe(1);
expect(testComp !.template.usedComponents.size).toBe(0);
expect(testImportComp !.template.usedComponents.size).toBe(1);
const [usedComp] = Array.from(testImportComp !.template.usedComponents);
expect(indexed.get(usedComp)).toEqual(testComp);
const [usedComp] = Array.from(testImportComp !.template.usedComponents);
expect(indexed.get(usedComp)).toEqual(testComp);
});
});
});
});

View File

@ -7,32 +7,17 @@
*/
import {CustomTransformers, Program} from '@angular/compiler-cli';
import {IndexedComponent} from '@angular/compiler-cli/src/ngtsc/indexer';
import {NgtscProgram} from '@angular/compiler-cli/src/ngtsc/program';
import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {createCompilerHost, createProgram} from '../../ngtools2';
import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main';
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';
import {LazyRoute} from '../../src/ngtsc/routing';
import {resolveNpmTreeArtifact} from '../runfile_helpers';
import {TestSupport, setup} from '../test_support';
import {setWrapHostForTest} from '../../src/transformers/compiler_host';
function setupFakeCore(support: TestSupport): void {
if (!process.env.TEST_SRCDIR) {
throw new Error('`setupFakeCore` must be run within a Bazel test');
}
const fakeNpmPackageDir =
resolveNpmTreeArtifact('angular/packages/compiler-cli/test/ngtsc/fake_core/npm_package');
const nodeModulesPath = path.join(support.basePath, 'node_modules');
const angularCoreDirectory = path.join(nodeModulesPath, '@angular/core');
fs.symlinkSync(fakeNpmPackageDir, angularCoreDirectory, 'junction');
}
/**
* Manages a temporary testing directory structure and environment for testing ngtsc by feeding it
@ -43,24 +28,24 @@ export class NgtscTestEnvironment {
private oldProgram: Program|null = null;
private changedResources: Set<string>|undefined = undefined;
private constructor(private support: TestSupport, readonly outDir: string) {}
get basePath(): string { return this.support.basePath; }
private constructor(
private fs: FileSystem, readonly outDir: AbsoluteFsPath, readonly basePath: AbsoluteFsPath) {}
/**
* Set up a new testing environment.
*/
static setup(): NgtscTestEnvironment {
const support = setup();
const outDir = path.posix.join(support.basePath, 'built');
process.chdir(support.basePath);
static setup(files?: Folder): NgtscTestEnvironment {
const fs = getFileSystem();
if (files !== undefined && fs instanceof MockFileSystem) {
fs.init(files);
}
setupFakeCore(support);
setWrapHostForTest(null);
const host = new AugmentedCompilerHost(fs);
setWrapHostForTest(makeWrapHost(host));
const env = new NgtscTestEnvironment(support, outDir);
const env = new NgtscTestEnvironment(fs, fs.resolve('/built'), absoluteFrom('/'));
env.write('tsconfig-base.json', `{
env.write(absoluteFrom('/tsconfig-base.json'), `{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
@ -91,26 +76,26 @@ export class NgtscTestEnvironment {
}
assertExists(fileName: string) {
if (!fs.existsSync(path.resolve(this.outDir, fileName))) {
if (!this.fs.exists(this.fs.resolve(this.outDir, fileName))) {
throw new Error(`Expected ${fileName} to be emitted (outDir: ${this.outDir})`);
}
}
assertDoesNotExist(fileName: string) {
if (fs.existsSync(path.resolve(this.outDir, fileName))) {
if (this.fs.exists(this.fs.resolve(this.outDir, fileName))) {
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${this.outDir})`);
}
}
getContents(fileName: string): string {
this.assertExists(fileName);
const modulePath = path.resolve(this.outDir, fileName);
return fs.readFileSync(modulePath, 'utf8');
const modulePath = this.fs.resolve(this.outDir, fileName);
return this.fs.readFile(modulePath);
}
enableMultipleCompilations(): void {
this.changedResources = new Set();
this.multiCompileHostExt = new MultiCompileHostExt();
this.multiCompileHostExt = new MultiCompileHostExt(this.fs);
setWrapHostForTest(makeWrapHost(this.multiCompileHostExt));
}
@ -126,31 +111,31 @@ export class NgtscTestEnvironment {
if (this.multiCompileHostExt === null) {
throw new Error(`Not tracking written files - call enableMultipleCompilations()`);
}
const outDir = path.posix.join(this.support.basePath, 'built');
const writtenFiles = new Set<string>();
this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => {
if (rawFile.startsWith(outDir)) {
writtenFiles.add(rawFile.substr(outDir.length));
if (rawFile.startsWith(this.outDir)) {
writtenFiles.add(rawFile.substr(this.outDir.length));
}
});
return writtenFiles;
}
write(fileName: string, content: string) {
const absFilePath = this.fs.resolve(this.basePath, fileName);
if (this.multiCompileHostExt !== null) {
const absFilePath = path.resolve(this.support.basePath, fileName).replace(/\\/g, '/');
this.multiCompileHostExt.invalidate(absFilePath);
this.changedResources !.add(absFilePath);
}
this.support.write(fileName, content);
this.fs.ensureDir(this.fs.dirname(absFilePath));
this.fs.writeFile(absFilePath, content);
}
invalidateCachedFile(fileName: string): void {
const absFilePath = this.fs.resolve(this.basePath, fileName);
if (this.multiCompileHostExt === null) {
throw new Error(`Not caching files - call enableMultipleCompilations()`);
}
const fullFile = path.posix.join(this.support.basePath, fileName);
this.multiCompileHostExt.invalidate(fullFile);
this.multiCompileHostExt.invalidate(absFilePath);
}
tsconfig(extraOpts: {[key: string]: string | boolean} = {}, extraRootDirs?: string[]): void {
@ -166,7 +151,7 @@ export class NgtscTestEnvironment {
this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2));
if (extraOpts['_useHostForImportGeneration'] === true) {
setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost()));
setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost(this.fs)));
}
}
@ -214,18 +199,15 @@ export class NgtscTestEnvironment {
}
}
class AugmentedCompilerHost {
class AugmentedCompilerHost extends NgtscCompilerHost {
delegate !: ts.CompilerHost;
}
class FileNameToModuleNameHost extends AugmentedCompilerHost {
// CWD must be initialized lazily as `this.delegate` is not set until later.
private cwd: string|null = null;
fileNameToModuleName(importedFilePath: string): string {
if (this.cwd === null) {
this.cwd = this.delegate.getCurrentDirectory();
}
return 'root' + importedFilePath.substr(this.cwd.length).replace(/(\.d)?.ts$/, '');
const relativeFilePath = this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath));
const rootedPath = this.fs.join('root', relativeFilePath);
return rootedPath.replace(/(\.d)?.ts$/, '');
}
}
@ -239,8 +221,7 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.Co
if (this.cache.has(fileName)) {
return this.cache.get(fileName) !;
}
const sf =
this.delegate.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
const sf = super.getSourceFile(fileName, languageVersion);
if (sf !== undefined) {
this.cache.set(sf.fileName, sf);
}
@ -253,7 +234,7 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.Co
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles?: ReadonlyArray<ts.SourceFile>): void {
this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
super.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
this.writtenFiles.add(fileName);
}

View File

@ -6,26 +6,31 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
import {NgtscTestEnvironment} from './env';
describe('ngtsc incremental compilation', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.enableMultipleCompilations();
env.tsconfig();
});
const testFiles = loadStandardTestFiles();
it('should skip unchanged services', () => {
env.write('service.ts', `
runInEachFileSystem(() => {
describe('ngtsc incremental compilation', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.enableMultipleCompilations();
env.tsconfig();
});
it('should skip unchanged services', () => {
env.write('service.ts', `
import {Injectable} from '@angular/core';
@Injectable()
export class Service {}
`);
env.write('test.ts', `
env.write('test.ts', `
import {Component} from '@angular/core';
import {Service} from './service';
@ -34,186 +39,186 @@ describe('ngtsc incremental compilation', () => {
constructor(service: Service) {}
}
`);
env.driveMain();
env.flushWrittenFileTracking();
env.driveMain();
env.flushWrittenFileTracking();
// Pretend a change was made to test.ts.
env.invalidateCachedFile('test.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
// Pretend a change was made to test.ts.
env.invalidateCachedFile('test.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
// The changed file should be recompiled, but not the service.
expect(written).toContain('/test.js');
expect(written).not.toContain('/service.js');
});
// The changed file should be recompiled, but not the service.
expect(written).toContain('/test.js');
expect(written).not.toContain('/service.js');
});
it('should rebuild components that have changed', () => {
env.write('component1.ts', `
it('should rebuild components that have changed', () => {
env.write('component1.ts', `
import {Component} from '@angular/core';
@Component({selector: 'cmp', template: 'cmp'})
export class Cmp1 {}
`);
env.write('component2.ts', `
env.write('component2.ts', `
import {Component} from '@angular/core';
@Component({selector: 'cmp2', template: 'cmp'})
export class Cmp2 {}
`);
env.driveMain();
env.driveMain();
// Pretend a change was made to Cmp1
env.flushWrittenFileTracking();
env.invalidateCachedFile('component1.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/component1.js');
expect(written).not.toContain('/component2.js');
});
// Pretend a change was made to Cmp1
env.flushWrittenFileTracking();
env.invalidateCachedFile('component1.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/component1.js');
expect(written).not.toContain('/component2.js');
});
it('should rebuild components whose templates have changed', () => {
env.write('component1.ts', `
it('should rebuild components whose templates have changed', () => {
env.write('component1.ts', `
import {Component} from '@angular/core';
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
export class Cmp1 {}
`);
env.write('component1.template.html', 'cmp1');
env.write('component2.ts', `
env.write('component1.template.html', 'cmp1');
env.write('component2.ts', `
import {Component} from '@angular/core';
@Component({selector: 'cmp2', templateUrl: './component2.template.html'})
export class Cmp2 {}
`);
env.write('component2.template.html', 'cmp2');
env.write('component2.template.html', 'cmp2');
env.driveMain();
env.driveMain();
// Make a change to Cmp1 template
env.flushWrittenFileTracking();
env.write('component1.template.html', 'changed');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/component1.js');
expect(written).not.toContain('/component2.js');
});
// Make a change to Cmp1 template
env.flushWrittenFileTracking();
env.write('component1.template.html', 'changed');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/component1.js');
expect(written).not.toContain('/component2.js');
});
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
env.write('component1.ts', `
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
env.write('component1.ts', `
import {Component} from '@angular/core';
@Component({selector: 'cmp', template: 'cmp'})
export class Cmp1 {}
`);
env.write('component2.ts', `
env.write('component2.ts', `
import {Component} from '@angular/core';
import {SELECTOR} from './constants';
@Component({selector: SELECTOR, template: 'cmp'})
export class Cmp2 {}
`);
env.write('constants.ts', `
env.write('constants.ts', `
export const SELECTOR = 'cmp';
`);
env.driveMain();
env.driveMain();
// Pretend a change was made to SELECTOR
env.flushWrittenFileTracking();
env.invalidateCachedFile('constants.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/constants.js');
expect(written).not.toContain('/component1.js');
expect(written).toContain('/component2.js');
// Pretend a change was made to SELECTOR
env.flushWrittenFileTracking();
env.invalidateCachedFile('constants.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/constants.js');
expect(written).not.toContain('/component1.js');
expect(written).toContain('/component2.js');
});
it('should rebuild components whose imported dependencies have changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to BarDir.
env.invalidateCachedFile('bar_directive.ts');
env.driveMain();
let written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js');
expect(written).toContain('/bar_module.js');
expect(written).not.toContain('/foo_component.js');
expect(written).not.toContain('/foo_pipe.js');
expect(written).not.toContain('/foo_module.js');
});
it('should rebuild components where their NgModule declared dependencies have changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to FooPipe.
env.invalidateCachedFile('foo_pipe.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js');
expect(written).not.toContain('/bar_component.js');
expect(written).not.toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should rebuild components where their NgModule has changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to FooPipe.
env.invalidateCachedFile('foo_module.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js');
expect(written).not.toContain('/bar_component.js');
expect(written).not.toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should rebuild everything if a typings file changes', () => {
setupFooBarProgram(env);
// Pretend a change was made to a typings file.
env.invalidateCachedFile('foo_selector.d.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js');
expect(written).toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should compile incrementally with template type-checking turned on', () => {
env.tsconfig({ivyTemplateTypeCheck: true});
env.write('main.ts', 'export class Foo {}');
env.driveMain();
env.invalidateCachedFile('main.ts');
env.driveMain();
// If program reuse were configured incorrectly (as was responsible for
// https://github.com/angular/angular/issues/30079), this would have crashed.
});
});
it('should rebuild components whose imported dependencies have changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to BarDir.
env.invalidateCachedFile('bar_directive.ts');
env.driveMain();
let written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js');
expect(written).toContain('/bar_module.js');
expect(written).not.toContain('/foo_component.js');
expect(written).not.toContain('/foo_pipe.js');
expect(written).not.toContain('/foo_module.js');
});
it('should rebuild components where their NgModule declared dependencies have changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to FooPipe.
env.invalidateCachedFile('foo_pipe.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js');
expect(written).not.toContain('/bar_component.js');
expect(written).not.toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should rebuild components where their NgModule has changed', () => {
setupFooBarProgram(env);
// Pretend a change was made to FooPipe.
env.invalidateCachedFile('foo_module.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js');
expect(written).not.toContain('/bar_component.js');
expect(written).not.toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should rebuild everything if a typings file changes', () => {
setupFooBarProgram(env);
// Pretend a change was made to a typings file.
env.invalidateCachedFile('foo_selector.d.ts');
env.driveMain();
const written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js');
expect(written).toContain('/bar_module.js');
expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js');
});
it('should compile incrementally with template type-checking turned on', () => {
env.tsconfig({ivyTemplateTypeCheck: true});
env.write('main.ts', 'export class Foo {}');
env.driveMain();
env.invalidateCachedFile('main.ts');
env.driveMain();
// If program reuse were configured incorrectly (as was responsible for
// https://github.com/angular/angular/issues/30079), this would have crashed.
});
});
function setupFooBarProgram(env: NgtscTestEnvironment) {
env.write('foo_component.ts', `
function setupFooBarProgram(env: NgtscTestEnvironment) {
env.write('foo_component.ts', `
import {Component} from '@angular/core';
import {fooSelector} from './foo_selector';
@Component({selector: fooSelector, template: 'foo'})
export class FooCmp {}
`);
env.write('foo_pipe.ts', `
env.write('foo_pipe.ts', `
import {Pipe} from '@angular/core';
@Pipe({name: 'foo'})
export class FooPipe {}
`);
env.write('foo_module.ts', `
env.write('foo_module.ts', `
import {NgModule} from '@angular/core';
import {FooCmp} from './foo_component';
import {FooPipe} from './foo_pipe';
@ -224,19 +229,19 @@ function setupFooBarProgram(env: NgtscTestEnvironment) {
})
export class FooModule {}
`);
env.write('bar_component.ts', `
env.write('bar_component.ts', `
import {Component} from '@angular/core';
@Component({selector: 'bar', template: 'bar'})
export class BarCmp {}
`);
env.write('bar_directive.ts', `
env.write('bar_directive.ts', `
import {Directive} from '@angular/core';
@Directive({selector: '[bar]'})
export class BarDir {}
`);
env.write('bar_module.ts', `
env.write('bar_module.ts', `
import {NgModule} from '@angular/core';
import {BarCmp} from './bar_component';
import {BarDir} from './bar_directive';
@ -246,9 +251,10 @@ function setupFooBarProgram(env: NgtscTestEnvironment) {
})
export class BarModule {}
`);
env.write('foo_selector.d.ts', `
env.write('foo_selector.d.ts', `
export const fooSelector = 'foo';
`);
env.driveMain();
env.flushWrittenFileTracking();
}
env.driveMain();
env.flushWrittenFileTracking();
}
});

File diff suppressed because it is too large Load Diff

View File

@ -5,25 +5,29 @@
* 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
*/
import * as ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
import {NgtscTestEnvironment} from './env';
describe('ngtsc module scopes', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.tsconfig();
});
const testFiles = loadStandardTestFiles();
describe('diagnostics', () => {
describe('imports', () => {
it('should emit imports in a pure function call', () => {
env.tsconfig();
env.write('test.ts', `
runInEachFileSystem(() => {
describe('ngtsc module scopes', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig();
});
describe('diagnostics', () => {
describe('imports', () => {
it('should emit imports in a pure function call', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
@ -33,22 +37,22 @@ describe('ngtsc module scopes', () => {
export class TestModule {}
`);
env.driveMain();
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });');
expect(jsContents)
.toContain(
'/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { imports: [OtherModule] });');
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });');
expect(jsContents)
.toContain(
'/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { imports: [OtherModule] });');
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta<TestModule, never, [typeof OtherModule], never>');
});
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta<TestModule, never, [typeof OtherModule], never>');
});
it('should produce an error when an invalid class is imported', () => {
env.write('test.ts', `
it('should produce an error when an invalid class is imported', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
class NotAModule {}
@ -56,36 +60,35 @@ describe('ngtsc module scopes', () => {
@NgModule({imports: [NotAModule]})
class IsAModule {}
`);
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.imports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_IMPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAModule');
});
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.imports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_IMPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAModule');
});
it('should produce an error when a non-class is imported from a .d.ts dependency', () => {
env.write('dep.d.ts', `export declare let NotAClass: Function;`);
env.write('test.ts', `
it('should produce an error when a non-class is imported from a .d.ts dependency', () => {
env.write('dep.d.ts', `export declare let NotAClass: Function;`);
env.write('test.ts', `
import {NgModule} from '@angular/core';
import {NotAClass} from './dep';
@NgModule({imports: [NotAClass]})
class IsAModule {}
`);
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.imports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAClass');
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.imports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.VALUE_HAS_WRONG_TYPE));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAClass');
});
});
});
describe('exports', () => {
it('should emit exports in a pure function call', () => {
env.tsconfig();
env.write('test.ts', `
describe('exports', () => {
it('should emit exports in a pure function call', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
@NgModule({})
@ -95,22 +98,22 @@ describe('ngtsc module scopes', () => {
export class TestModule {}
`);
env.driveMain();
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });');
expect(jsContents)
.toContain(
'/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { exports: [OtherModule] });');
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });');
expect(jsContents)
.toContain(
'/*@__PURE__*/ i0.ɵɵsetNgModuleScope(TestModule, { exports: [OtherModule] });');
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta<TestModule, never, never, [typeof OtherModule]>');
});
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents)
.toContain(
'static ngModuleDef: i0.ɵɵNgModuleDefWithMeta<TestModule, never, never, [typeof OtherModule]>');
});
it('should produce an error when a non-NgModule class is exported', () => {
env.write('test.ts', `
it('should produce an error when a non-NgModule class is exported', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
class NotAModule {}
@ -118,16 +121,16 @@ describe('ngtsc module scopes', () => {
@NgModule({exports: [NotAModule]})
class IsAModule {}
`);
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_EXPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAModule');
});
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_EXPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('NotAModule');
});
it('should produce a transitive error when an invalid NgModule is exported', () => {
env.write('test.ts', `
it('should produce a transitive error when an invalid NgModule is exported', () => {
env.write('test.ts', `
import {NgModule} from '@angular/core';
export class NotAModule {}
@ -141,21 +144,21 @@ describe('ngtsc module scopes', () => {
class IsAModule {}
`);
// Find the diagnostic referencing InvalidModule, which should have come from IsAModule.
const error = env.driveDiagnostics().find(
error => diagnosticToNode(error, ts.isIdentifier).text === 'InvalidModule');
if (error === undefined) {
return fail('Expected to find a diagnostic referencing InvalidModule');
}
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_EXPORT));
// Find the diagnostic referencing InvalidModule, which should have come from IsAModule.
const error = env.driveDiagnostics().find(
error => diagnosticToNode(error, ts.isIdentifier).text === 'InvalidModule');
if (error === undefined) {
return fail('Expected to find a diagnostic referencing InvalidModule');
}
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_EXPORT));
});
});
});
describe('re-exports', () => {
it('should produce an error when a non-declared/imported class is re-exported', () => {
env.write('test.ts', `
describe('re-exports', () => {
it('should produce an error when a non-declared/imported class is re-exported', () => {
env.write('test.ts', `
import {Directive, NgModule} from '@angular/core';
@Directive({selector: 'test'})
@ -164,23 +167,24 @@ describe('ngtsc module scopes', () => {
@NgModule({exports: [Dir]})
class IsAModule {}
`);
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_REEXPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('Dir');
const [error] = env.driveDiagnostics();
expect(error).not.toBeUndefined();
expect(error.messageText).toContain('IsAModule');
expect(error.messageText).toContain('NgModule.exports');
expect(error.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_INVALID_REEXPORT));
expect(diagnosticToNode(error, ts.isIdentifier).text).toEqual('Dir');
});
});
});
});
});
function diagnosticToNode<T extends ts.Node>(
diag: ts.Diagnostic, guard: (node: ts.Node) => node is T): T {
if (diag.file === undefined) {
throw new Error(`Expected ts.Diagnostic to have a file source`);
function diagnosticToNode<T extends ts.Node>(
diag: ts.Diagnostic, guard: (node: ts.Node) => node is T): T {
if (diag.file === undefined) {
throw new Error(`Expected ts.Diagnostic to have a file source`);
}
const node = (ts as any).getTokenAtPosition(diag.file, diag.start) as ts.Node;
expect(guard(node)).toBe(true);
return node as T;
}
const node = (ts as any).getTokenAtPosition(diag.file, diag.start) as ts.Node;
expect(guard(node)).toBe(true);
return node as T;
}
});

View File

@ -5,7 +5,6 @@
* 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
*/
import {MappingItem, SourceMapConsumer} from 'source-map';
import {NgtscTestEnvironment} from './env';

File diff suppressed because it is too large Load Diff

View File

@ -8,10 +8,21 @@
import * as ts from 'typescript';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
import {NgtscTestEnvironment} from './env';
function setupCommon(env: NgtscTestEnvironment): void {
env.write('node_modules/@angular/common/index.d.ts', `
const testFiles = loadStandardTestFiles();
runInEachFileSystem(() => {
describe('ngtsc type checking', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({fullTemplateTypeCheck: true});
env.write('node_modules/@angular/common/index.d.ts', `
import * as i0 from '@angular/core';
export declare class NgForOfContext<T> {
@ -47,19 +58,10 @@ export declare class CommonModule {
static ngModuleDef: i0.ɵɵNgModuleDefWithMeta<CommonModule, [typeof NgIf, typeof NgForOf, typeof IndexPipe], never, [typeof NgIf, typeof NgForOf, typeof IndexPipe]>;
}
`);
}
});
describe('ngtsc type checking', () => {
let env !: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup();
env.tsconfig({fullTemplateTypeCheck: true});
setupCommon(env);
});
it('should check a simple component', () => {
env.write('test.ts', `
it('should check a simple component', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
@ -74,11 +76,11 @@ describe('ngtsc type checking', () => {
class Module {}
`);
env.driveMain();
});
env.driveMain();
});
it('should check basic usage of NgIf', () => {
env.write('test.ts', `
it('should check basic usage of NgIf', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -97,11 +99,11 @@ describe('ngtsc type checking', () => {
class Module {}
`);
env.driveMain();
});
env.driveMain();
});
it('should check usage of NgIf with explicit non-null guard', () => {
env.write('test.ts', `
it('should check usage of NgIf with explicit non-null guard', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -120,11 +122,11 @@ describe('ngtsc type checking', () => {
class Module {}
`);
env.driveMain();
});
env.driveMain();
});
it('should check basic usage of NgFor', () => {
env.write('test.ts', `
it('should check basic usage of NgFor', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -143,11 +145,11 @@ describe('ngtsc type checking', () => {
class Module {}
`);
env.driveMain();
});
env.driveMain();
});
it('should report an error inside the NgFor template', () => {
env.write('test.ts', `
it('should report an error inside the NgFor template', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -166,13 +168,13 @@ describe('ngtsc type checking', () => {
export class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('does_not_exist');
});
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('does_not_exist');
});
it('should accept an NgFor iteration over an any-typed value', () => {
env.write('test.ts', `
it('should accept an NgFor iteration over an any-typed value', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -191,11 +193,11 @@ describe('ngtsc type checking', () => {
export class Module {}
`);
env.driveMain();
});
env.driveMain();
});
it('should report an error with pipe bindings', () => {
env.write('test.ts', `
it('should report an error with pipe bindings', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@ -227,26 +229,27 @@ describe('ngtsc type checking', () => {
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(4);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(4);
const allErrors = [
`'does_not_exist' does not exist on type '{ name: string; }'`,
`Expected 2 arguments, but got 3.`,
`Argument of type '"test"' is not assignable to parameter of type 'number'`,
`Argument of type '{ name: string; }' is not assignable to parameter of type '{}[]'`,
];
const allErrors = [
`'does_not_exist' does not exist on type '{ name: string; }'`,
`Expected 2 arguments, but got 3.`,
`Argument of type '"test"' is not assignable to parameter of type 'number'`,
`Argument of type '{ name: string; }' is not assignable to parameter of type '{}[]'`,
];
for (const error of allErrors) {
if (!diags.some(
diag => ts.flattenDiagnosticMessageText(diag.messageText, '').indexOf(error) > -1)) {
fail(`Expected a diagnostic message with text: ${error}`);
for (const error of allErrors) {
if (!diags.some(
diag =>
ts.flattenDiagnosticMessageText(diag.messageText, '').indexOf(error) > -1)) {
fail(`Expected a diagnostic message with text: ${error}`);
}
}
}
});
});
it('should constrain types using type parameter bounds', () => {
env.write('test.ts', `
it('should constrain types using type parameter bounds', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, Input, NgModule} from '@angular/core';
@ -265,14 +268,14 @@ describe('ngtsc type checking', () => {
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('does_not_exist');
});
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('does_not_exist');
});
it('should property type-check a microsyntax variable with the same name as the expression',
() => {
env.write('test.ts', `
it('should property type-check a microsyntax variable with the same name as the expression',
() => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, Input, NgModule} from '@angular/core';
@ -291,12 +294,12 @@ describe('ngtsc type checking', () => {
export class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should properly type-check inherited directives', () => {
env.write('test.ts', `
it('should properly type-check inherited directives', () => {
env.write('test.ts', `
import {Component, Directive, Input, NgModule} from '@angular/core';
@Directive({
@ -325,15 +328,16 @@ describe('ngtsc type checking', () => {
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(2);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(2);
// Error from the binding to [fromBase].
expect(diags[0].messageText)
.toBe(`Type 'number' is not assignable to type 'string | undefined'.`);
// Error from the binding to [fromBase].
expect(diags[0].messageText)
.toBe(`Type 'number' is not assignable to type 'string | undefined'.`);
// Error from the binding to [fromChild].
expect(diags[1].messageText)
.toBe(`Type 'number' is not assignable to type 'boolean | undefined'.`);
// Error from the binding to [fromChild].
expect(diags[1].messageText)
.toBe(`Type 'number' is not assignable to type 'boolean | undefined'.`);
});
});
});

View File

@ -5,12 +5,14 @@
* 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
*/
/// <reference types="node" />
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import * as ng from '../index';
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from './runfile_helpers';
import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../test/helpers';
// TEST_TMPDIR is always set by Bazel.
const tmpdir = process.env.TEST_TMPDIR !;
@ -143,6 +145,9 @@ export function setupBazelTo(tmpDirPath: string) {
}
export function setup(): TestSupport {
// // `TestSupport` provides its own file-system abstraction so we just use
// // the native `NodeJSFileSystem` under the hood.
setFileSystem(new NodeJSFileSystem());
const tmpDirPath = makeTempDir();
setupBazelTo(tmpDirPath);
return createTestSupportFor(tmpDirPath);

View File

@ -5,12 +5,11 @@
* 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
*/
/// <reference types="node" />
import * as ng from '@angular/compiler-cli';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {formatDiagnostics} from '../../src/perform_compile';
import {CompilerHost, EmitFlags, LazyRoute} from '../../src/transformers/api';
import {checkVersion, createSrcToOutPathMapper} from '../../src/transformers/program';