Update the license headers throughout the repository to reference Google LLC rather than Google Inc, for the required license headers. PR Close #37205
		
			
				
	
	
		
			532 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			532 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 '../helpers/src/mock_file_loading';
 | 
						|
 | 
						|
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 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 to have a different selector, isn't matched in BCmp's template.
 | 
						|
        env.write('a.ts', `
 | 
						|
          import {Component} from '@angular/core';
 | 
						|
 | 
						|
          @Component({selector: 'not-a-cmp', 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 ? '{}' : ''}
 | 
						|
  `);
 | 
						|
}
 |