A couple reasons to justify removing the flag: * It adds code to the compiler that is only meant to support test cases and not any production. We should avoid code in that's only meant to support tests. * The flag enables writing tests that do not mimic real-world behavior because they allow invalid applications PR Close #40013
		
			
				
	
	
		
			564 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			564 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC 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
 | |
|  */
 | |
| 
 | |
| import {absoluteFrom as _} from '../../src/ngtsc/file_system';
 | |
| import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
 | |
| import {loadStandardTestFiles} from '../../src/ngtsc/testing';
 | |
| 
 | |
| import {NgtscTestEnvironment} from './env';
 | |
| 
 | |
| const testFiles = loadStandardTestFiles();
 | |
| 
 | |
| runInEachFileSystem(() => {
 | |
|   describe('ngtsc incremental compilation with errors', () => {
 | |
|     let env!: NgtscTestEnvironment;
 | |
| 
 | |
|     beforeEach(() => {
 | |
|       env = NgtscTestEnvironment.setup(testFiles);
 | |
|       env.enableMultipleCompilations();
 | |
|       env.tsconfig();
 | |
| 
 | |
|       // This file is part of the program, but not referenced by anything else. It can be used by
 | |
|       // each test to verify that it isn't re-emitted after incremental builds.
 | |
|       env.write('unrelated.ts', `
 | |
|         export class Unrelated {}
 | |
|       `);
 | |
|     });
 | |
| 
 | |
|     function expectToHaveWritten(files: string[]): void {
 | |
|       const set = env.getFilesWrittenSinceLastFlush();
 | |
| 
 | |
|       const expectedSet = new Set<string>();
 | |
|       for (const file of files) {
 | |
|         expectedSet.add(file);
 | |
|         expectedSet.add(file.replace(/\.js$/, '.d.ts'));
 | |
|       }
 | |
| 
 | |
|       expect(set).toEqual(expectedSet);
 | |
| 
 | |
|       // Reset for the next compilation.
 | |
|       env.flushWrittenFileTracking();
 | |
|     }
 | |
| 
 | |
|     it('should handle an error in an unrelated file', () => {
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('other.ts', `
 | |
|         export class Other {}
 | |
|       `);
 | |
| 
 | |
|       // Start with a clean compilation.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       // Introduce the error.
 | |
|       env.write('other.ts', `
 | |
|         export class Other // missing braces
 | |
|       `);
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBe(1);
 | |
|       expect(diags[0].file!.fileName).toBe(_('/other.ts'));
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Remove the error. /other.js should now be emitted again.
 | |
|       env.write('other.ts', `
 | |
|         export class Other {}
 | |
|       `);
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten(['/other.js']);
 | |
|     });
 | |
| 
 | |
|     it('should emit all files after an error on the initial build', () => {
 | |
|       // Intentionally start with a broken compilation.
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('other.ts', `
 | |
|         export class Other // missing braces
 | |
|       `);
 | |
| 
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBe(1);
 | |
|       expect(diags[0].file!.fileName).toBe(_('/other.ts'));
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Remove the error. All files should be emitted.
 | |
|       env.write('other.ts', `
 | |
|         export class Other {}
 | |
|       `);
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten(['/cmp.js', '/other.js', '/unrelated.js']);
 | |
|     });
 | |
| 
 | |
|     it('should emit files introduced at the same time as an unrelated error', () => {
 | |
|       env.write('other.ts', `
 | |
|         // Needed so that the initial program contains @angular/core's .d.ts file.
 | |
|         import '@angular/core';
 | |
| 
 | |
|         export class Other {}
 | |
|       `);
 | |
| 
 | |
|       // Clean compile.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('other.ts', `
 | |
|         export class Other // missing braces
 | |
|       `);
 | |
| 
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBe(1);
 | |
|       expect(diags[0].file!.fileName).toBe(_('/other.ts'));
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Remove the error. All files should be emitted.
 | |
|       env.write('other.ts', `
 | |
|         export class Other {}
 | |
|       `);
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten(['/cmp.js', '/other.js']);
 | |
|     });
 | |
| 
 | |
|     it('should emit dependent files even in the face of an error', () => {
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
|         import {SELECTOR} from './selector';
 | |
| 
 | |
|         @Component({selector: SELECTOR, template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('selector.ts', `
 | |
|         export const SELECTOR = 'test-cmp';
 | |
|       `);
 | |
| 
 | |
|       env.write('other.ts', `
 | |
|         // Needed so that the initial program contains @angular/core's .d.ts file.
 | |
|         import '@angular/core';
 | |
| 
 | |
|         export class Other {}
 | |
|       `);
 | |
| 
 | |
|       // Clean compile.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('other.ts', `
 | |
|         export class Other // missing braces
 | |
|       `);
 | |
| 
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBe(1);
 | |
|       expect(diags[0].file!.fileName).toBe(_('/other.ts'));
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Remove the error. All files should be emitted.
 | |
|       env.write('other.ts', `
 | |
|         export class Other {}
 | |
|       `);
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten(['/cmp.js', '/other.js']);
 | |
|     });
 | |
| 
 | |
|     it('should recover from an error in a component\'s metadata', () => {
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
| 
 | |
|       // Start with a clean compilation.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       // Introduce the error.
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: ...}) // invalid template
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBeGreaterThan(0);
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Clear the error and verify that the compiler now emits test.js again.
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
| 
 | |
|       env.driveMain();
 | |
|       expectToHaveWritten(['/test.js']);
 | |
|     });
 | |
| 
 | |
|     it('should recover from an error in a component that is part of a module', () => {
 | |
|       // In this test, there are two components, TestCmp and TargetCmp, that are part of the same
 | |
|       // NgModule. TestCmp is broken in an incremental build and then fixed, and the test verifies
 | |
|       // that TargetCmp is re-emitted.
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       env.write('target.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'target-cmp', template: '<test-cmp></test-cmp>'})
 | |
|         export class TargetCmp {}
 | |
|       `);
 | |
|       env.write('module.ts', `
 | |
|         import {NgModule} from '@angular/core';
 | |
|         import {TargetCmp} from './target';
 | |
|         import {TestCmp} from './test';
 | |
| 
 | |
|         @NgModule({
 | |
|           declarations: [TestCmp, TargetCmp],
 | |
|         })
 | |
|         export class Module {}
 | |
|       `);
 | |
| 
 | |
|       // Start with a clean compilation.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       // Introduce the syntactic error.
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: ..., template: '...'}) // ... is not valid syntax
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBeGreaterThan(0);
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Clear the error and trigger the rebuild.
 | |
|       env.write('test.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class TestCmp {}
 | |
|       `);
 | |
| 
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten([
 | |
|         // The file which had the error should have been emitted, of course.
 | |
|         '/test.js',
 | |
| 
 | |
|         // Because TestCmp belongs to a module, the module's file should also have been
 | |
|         // re-emitted.
 | |
|         '/module.js',
 | |
| 
 | |
|         // Because TargetCmp also belongs to the same module, it should be re-emitted since
 | |
|         // TestCmp's elector may have changed.
 | |
|         '/target.js',
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     it('should recover from an error in an external template', () => {
 | |
|       env.write('mod.ts', `
 | |
|         import {NgModule} from '@angular/core';
 | |
|         import {Cmp} from './cmp';
 | |
| 
 | |
|         @NgModule({
 | |
|           declarations: [Cmp],
 | |
|         })
 | |
|         export class Mod {}
 | |
|       `);
 | |
|       env.write('cmp.html', '{{ error = "true" }} ');
 | |
|       env.write('cmp.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({
 | |
|           templateUrl: './cmp.html',
 | |
|           selector: 'some-cmp',
 | |
|         })
 | |
|         export class Cmp {
 | |
|           error = 'false';
 | |
|         }
 | |
|       `);
 | |
| 
 | |
|       // Diagnostics should show for the broken component template.
 | |
|       expect(env.driveDiagnostics().length).toBeGreaterThan(0);
 | |
| 
 | |
|       env.write('cmp.html', '{{ error }} ');
 | |
| 
 | |
|       // There should be no diagnostics.
 | |
|       env.driveMain();
 | |
|     });
 | |
| 
 | |
|     it('should recover from an error even across multiple NgModules', () => {
 | |
|       // This test is a variation on the above. Two components (CmpA and CmpB) exist in an NgModule,
 | |
|       // which indirectly imports a LibModule (via another NgModule in the middle). The test is
 | |
|       // designed to verify that CmpA and CmpB are re-emitted if somewhere upstream in the NgModule
 | |
|       // graph, an error is fixed. To check this, LibModule is broken and then fixed in incremental
 | |
|       // build steps.
 | |
|       env.write('a.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'test-cmp', template: '...'})
 | |
|         export class CmpA {}
 | |
|       `);
 | |
|       env.write('b.ts', `
 | |
|         import {Component} from '@angular/core';
 | |
| 
 | |
|         @Component({selector: 'target-cmp', template: '...'})
 | |
|         export class CmpB {}
 | |
|       `);
 | |
|       env.write('module.ts', `
 | |
|         import {NgModule} from '@angular/core';
 | |
|         import {LibModule} from './lib';
 | |
|         import {CmpA} from './a';
 | |
|         import {CmpB} from './b';
 | |
| 
 | |
|         @NgModule({
 | |
|           imports: [LibModule],
 | |
|           exports: [LibModule],
 | |
|         })
 | |
|         export class IndirectModule {}
 | |
| 
 | |
|         @NgModule({
 | |
|           declarations: [CmpA, CmpB],
 | |
|           imports: [IndirectModule],
 | |
|         })
 | |
|         export class Module {}
 | |
|       `);
 | |
|       env.write('lib.ts', `
 | |
|         import {Component, NgModule} from '@angular/core';
 | |
| 
 | |
|         @Component({
 | |
|           selector: 'lib-cmp',
 | |
|           template: '...',
 | |
|         })
 | |
|         export class LibCmp {}
 | |
| 
 | |
|         @NgModule({
 | |
|           declarations: [LibCmp],
 | |
|           exports: [LibCmp],
 | |
|         })
 | |
|         export class LibModule {}
 | |
|       `);
 | |
| 
 | |
|       // Start with a clean compilation.
 | |
|       env.driveMain();
 | |
|       env.flushWrittenFileTracking();
 | |
| 
 | |
|       // Introduce the error in LibModule
 | |
|       env.write('lib.ts', `
 | |
|       import {Component, NgModule} from '@angular/core';
 | |
| 
 | |
|       @Component({
 | |
|         selector: 'lib-cmp',
 | |
|         template: '...',
 | |
|       })
 | |
|       export class LibCmp {}
 | |
| 
 | |
|       @NgModule({
 | |
|         declarations: [LibCmp],
 | |
|         exports: [LibCmp],
 | |
|       })
 | |
|       export class LibModule // missing braces
 | |
|       `);
 | |
|       // env.driveMain();
 | |
|       const diags = env.driveDiagnostics();
 | |
|       expect(diags.length).toBeGreaterThan(0);
 | |
|       expectToHaveWritten([]);
 | |
| 
 | |
|       // Clear the error and recompile.
 | |
|       env.write('lib.ts', `
 | |
|       import {Component, NgModule} from '@angular/core';
 | |
| 
 | |
|       @Component({
 | |
|         selector: 'lib-cmp',
 | |
|         template: '...',
 | |
|       })
 | |
|       export class LibCmp {}
 | |
| 
 | |
|       @NgModule({
 | |
|         declarations: [LibCmp],
 | |
|         exports: [LibCmp],
 | |
|       })
 | |
|       export class LibModule {}
 | |
|       `);
 | |
| 
 | |
|       env.driveMain();
 | |
| 
 | |
|       expectToHaveWritten([
 | |
|         // Both CmpA and CmpB should be re-emitted.
 | |
|         '/a.js',
 | |
|         '/b.js',
 | |
| 
 | |
|         // So should the module itself.
 | |
|         '/module.js',
 | |
| 
 | |
|         // And of course, the file with the error.
 | |
|         '/lib.js',
 | |
|       ]);
 | |
|     });
 | |
| 
 | |
|     describe('chained errors', () => {
 | |
|       it('should remember a change to a TS file across broken builds', () => {
 | |
|         // Two components, an NgModule, and a random file.
 | |
|         writeTwoComponentSystem(env);
 | |
|         writeRandomFile(env, 'other.ts');
 | |
| 
 | |
|         // Start with a clean build.
 | |
|         env.driveMain();
 | |
|         env.flushWrittenFileTracking();
 | |
| 
 | |
|         // Update ACmp
 | |
|         env.write('a.ts', `
 | |
|           import {Component} from '@angular/core';
 | |
| 
 | |
|           @Component({selector: 'a-cmp', template: 'new template'})
 | |
|           export class ACmp {}
 | |
|        `);
 | |
| 
 | |
|         // Update the file to have an error, simultaneously.
 | |
|         writeRandomFile(env, 'other.ts', {error: true});
 | |
| 
 | |
|         // This build should fail.
 | |
|         const diags = env.driveDiagnostics();
 | |
|         expect(diags.length).not.toBe(0);
 | |
|         expectToHaveWritten([]);
 | |
| 
 | |
|         // Fix the error.
 | |
|         writeRandomFile(env, 'other.ts');
 | |
| 
 | |
|         // Rebuild.
 | |
|         env.driveMain();
 | |
| 
 | |
|         // If the compiler behaves correctly, it should remember that 'a.ts' was updated before, and
 | |
|         // should regenerate b.ts.
 | |
|         expectToHaveWritten([
 | |
|           // Because they directly changed
 | |
|           '/other.js',
 | |
|           '/a.js',
 | |
| 
 | |
|           // Bcause they depend on a.ts
 | |
|           '/b.js',
 | |
|           '/module.js',
 | |
|         ]);
 | |
|       });
 | |
| 
 | |
|       it('should remember a change to a template file across broken builds', () => {
 | |
|         // This is basically the same test as above, except a.html is changed instead of a.ts.
 | |
| 
 | |
|         // Two components, an NgModule, and a random file.
 | |
|         writeTwoComponentSystem(env);
 | |
|         writeRandomFile(env, 'other.ts');
 | |
| 
 | |
|         // Start with a clean build.
 | |
|         env.driveMain();
 | |
|         env.flushWrittenFileTracking();
 | |
| 
 | |
|         // Invalidate ACmp's template.
 | |
|         env.write('a.html', 'Changed template');
 | |
| 
 | |
|         // Update the file to have an error, simultaneously.
 | |
|         writeRandomFile(env, 'other.ts', {error: true});
 | |
| 
 | |
|         // This build should fail.
 | |
|         const diags = env.driveDiagnostics();
 | |
|         expect(diags.length).not.toBe(0);
 | |
|         expectToHaveWritten([]);
 | |
| 
 | |
|         // Fix the error.
 | |
|         writeRandomFile(env, 'other.ts');
 | |
| 
 | |
|         // Rebuild.
 | |
|         env.flushWrittenFileTracking();
 | |
|         env.driveMain();
 | |
| 
 | |
|         // If the compiler behaves correctly, it should remember that 'a.html' was updated before,
 | |
|         // and should regenerate a.js. Because the compiler knows a.html is a _resource_ dependency
 | |
|         // of a.ts, it should only regenerate a.js and not its module and dependent components (as
 | |
|         // it would if a.ts were itself changed like in the test above).
 | |
|         expectToHaveWritten([
 | |
|           // Because it directly changed.
 | |
|           '/other.js',
 | |
| 
 | |
|           // Because a.html changed
 | |
|           '/a.js', '/module.js',
 | |
| 
 | |
|           // b.js and module.js should not be re-emitted, because specifically when tracking
 | |
|           // resource dependencies, the compiler knows that a change to a resource file only affects
 | |
|           // the direct emit of dependent file.
 | |
|         ]);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Two components, ACmp and BCmp, where BCmp depends on ACmp.
 | |
|  *
 | |
|  * ACmp has its template in a separate file.
 | |
|  */
 | |
| export function writeTwoComponentSystem(env: NgtscTestEnvironment): void {
 | |
|   env.write('a.html', 'This is the template for CmpA');
 | |
|   env.write('a.ts', `
 | |
|     import {Component} from '@angular/core';
 | |
| 
 | |
|     @Component({selector: 'a-cmp', templateUrl: './a.html'})
 | |
|     export class ACmp {}
 | |
|   `);
 | |
|   env.write('b.ts', `
 | |
|     import {Component} from '@angular/core';
 | |
| 
 | |
|     @Component({selector: 'b-cmp', template: '<a-cmp></a-cmp>'})
 | |
|     export class BCmp {}
 | |
|   `);
 | |
|   env.write('module.ts', `
 | |
|     import {NgModule} from '@angular/core';
 | |
|     import {ACmp} from './a';
 | |
|     import {BCmp} from './b';
 | |
| 
 | |
|     @NgModule({
 | |
|       declarations: [ACmp, BCmp],
 | |
|     })
 | |
|     export class Module {}
 | |
| `);
 | |
| }
 | |
| 
 | |
| export function writeRandomFile(
 | |
|     env: NgtscTestEnvironment, name: string, options: {error?: true} = {}): void {
 | |
|   env.write(name, `
 | |
|     // If options.error is set, this class has missing braces.
 | |
|     export class Other ${options.error !== true ? '{}' : ''}
 | |
|   `);
 | |
| }
 |