This is a precursor to introducing the Angular linker. As an initial step, a compiler option to configure the compilation mode is introduced. This option is initially internal until the linker is considered ready. PR Close #38938
		
			
				
	
	
		
			234 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			234 lines
		
	
	
		
			9.6 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 * as ts from 'typescript';
 | |
| 
 | |
| import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
 | |
| import {absoluteFrom} from '../../../src/ngtsc/file_system';
 | |
| import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
 | |
| import {MockLogger} from '../../../src/ngtsc/logging/testing';
 | |
| import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
 | |
| import {getDeclaration} from '../../../src/ngtsc/testing';
 | |
| import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, TraitState} from '../../../src/ngtsc/transform';
 | |
| import {loadTestFiles} from '../../../test/helpers';
 | |
| import {DefaultMigrationHost} from '../../src/analysis/migration_host';
 | |
| import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler';
 | |
| import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
 | |
| import {createComponentDecorator} from '../../src/migrations/utils';
 | |
| import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
 | |
| import {makeTestEntryPointBundle} from '../helpers/utils';
 | |
| 
 | |
| runInEachFileSystem(() => {
 | |
|   describe('DefaultMigrationHost', () => {
 | |
|     let _: typeof absoluteFrom;
 | |
|     let mockMetadata: any = {};
 | |
|     let mockEvaluator: any = {};
 | |
|     let mockClazz: any;
 | |
|     let injectedDecorator: any = {name: 'InjectedDecorator'};
 | |
|     beforeEach(() => {
 | |
|       _ = absoluteFrom;
 | |
|       const mockSourceFile: any = {
 | |
|         fileName: _('/node_modules/some-package/entry-point/test-file.js'),
 | |
|       };
 | |
|       mockClazz = {
 | |
|         name: {text: 'MockClazz'},
 | |
|         getSourceFile: () => mockSourceFile,
 | |
|         getStart: () => 0,
 | |
|         getWidth: () => 0,
 | |
|       };
 | |
|     });
 | |
| 
 | |
|     function createMigrationHost({entryPoint, handlers}: {
 | |
|       entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
 | |
|     }) {
 | |
|       const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
 | |
|       const compiler = new NgccTraitCompiler(handlers, reflectionHost);
 | |
|       const host = new DefaultMigrationHost(
 | |
|           reflectionHost, mockMetadata, mockEvaluator, compiler, entryPoint.entryPoint.path);
 | |
|       return {compiler, host};
 | |
|     }
 | |
| 
 | |
|     describe('injectSyntheticDecorator()', () => {
 | |
|       it('should add the injected decorator into the compilation', () => {
 | |
|         const handler = new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
 | |
|         loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
 | |
|         host.injectSyntheticDecorator(mockClazz, injectedDecorator);
 | |
| 
 | |
|         const record = compiler.recordFor(mockClazz)!;
 | |
|         expect(record).toBeDefined();
 | |
|         expect(record.traits.length).toBe(1);
 | |
|         expect(record.traits[0].detected.decorator).toBe(injectedDecorator);
 | |
|       });
 | |
| 
 | |
|       it('should mention the migration that failed in the diagnostics message', () => {
 | |
|         const handler = new DiagnosticProducingHandler();
 | |
|         loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host, compiler} = createMigrationHost({entryPoint, handlers: [handler]});
 | |
|         const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
 | |
|         host.injectSyntheticDecorator(mockClazz, decorator);
 | |
| 
 | |
|         const record = compiler.recordFor(mockClazz)!;
 | |
|         const migratedTrait = record.traits[0];
 | |
|         if (migratedTrait.state !== TraitState.ERRORED) {
 | |
|           return fail('Expected migrated class trait to be in an error state');
 | |
|         }
 | |
| 
 | |
|         expect(migratedTrait.diagnostics.length).toBe(1);
 | |
|         expect(ts.flattenDiagnosticMessageText(migratedTrait.diagnostics[0].messageText, '\n'))
 | |
|             .toEqual(
 | |
|                 `test diagnostic\n` +
 | |
|                 `  Occurs for @Component decorator inserted by an automatic migration\n` +
 | |
|                 `  @Component({ template: "", selector: "comp" })`);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('getAllDecorators', () => {
 | |
|       it('should include injected decorators', () => {
 | |
|         const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
 | |
|         const injectedHandler =
 | |
|             new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
 | |
|         loadTestFiles([{
 | |
|           name: _('/node_modules/test/index.js'),
 | |
|           contents: `
 | |
|             import {Directive} from '@angular/core';
 | |
| 
 | |
|             export class MyClass {};
 | |
|             MyClass.decorators = [{ type: Directive }];
 | |
|           `
 | |
|         }]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host, compiler} =
 | |
|             createMigrationHost({entryPoint, handlers: [directiveHandler, injectedHandler]});
 | |
|         const myClass = getDeclaration(
 | |
|             entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
 | |
|             isNamedClassDeclaration);
 | |
| 
 | |
|         compiler.analyzeFile(entryPoint.src.file);
 | |
| 
 | |
|         host.injectSyntheticDecorator(myClass, injectedDecorator);
 | |
| 
 | |
|         const decorators = host.getAllDecorators(myClass)!;
 | |
|         expect(decorators.length).toBe(2);
 | |
|         expect(decorators[0].name).toBe('Directive');
 | |
|         expect(decorators[1].name).toBe('InjectedDecorator');
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('isInScope', () => {
 | |
|       it('should be true for nodes within the entry-point', () => {
 | |
|         loadTestFiles([
 | |
|           {name: _('/node_modules/test/index.js'), contents: `export * from './internal';`},
 | |
|           {name: _('/node_modules/test/internal.js'), contents: `export class InternalClass {}`},
 | |
|         ]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host} = createMigrationHost({entryPoint, handlers: []});
 | |
|         const internalClass = getDeclaration(
 | |
|             entryPoint.src.program, _('/node_modules/test/internal.js'), 'InternalClass',
 | |
|             isNamedClassDeclaration);
 | |
| 
 | |
|         expect(host.isInScope(internalClass)).toBe(true);
 | |
|       });
 | |
| 
 | |
|       it('should be false for nodes outside the entry-point (in sibling package)', () => {
 | |
|         loadTestFiles([
 | |
|           {name: _('/node_modules/external/index.js'), contents: `export class ExternalClass {}`},
 | |
|           {
 | |
|             name: _('/node_modules/test/index.js'),
 | |
|             contents: `
 | |
|               export {ExternalClass} from 'external';
 | |
|               export class InternalClass {}
 | |
|             `
 | |
|           },
 | |
|         ]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host} = createMigrationHost({entryPoint, handlers: []});
 | |
|         const externalClass = getDeclaration(
 | |
|             entryPoint.src.program, _('/node_modules/external/index.js'), 'ExternalClass',
 | |
|             isNamedClassDeclaration);
 | |
| 
 | |
|         expect(host.isInScope(externalClass)).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('should be false for nodes outside the entry-point (in nested `node_modules/`)', () => {
 | |
|         loadTestFiles([
 | |
|           {
 | |
|             name: _('/node_modules/test/index.js'),
 | |
|             contents: `
 | |
|               export {NestedDependencyClass} from 'nested';
 | |
|               export class InternalClass {}
 | |
|             `,
 | |
|           },
 | |
|           {
 | |
|             name: _('/node_modules/test/node_modules/nested/index.js'),
 | |
|             contents: `export class NestedDependencyClass {}`,
 | |
|           },
 | |
|         ]);
 | |
|         const entryPoint =
 | |
|             makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
 | |
|         const {host} = createMigrationHost({entryPoint, handlers: []});
 | |
|         const nestedDepClass = getDeclaration(
 | |
|             entryPoint.src.program, _('/node_modules/test/node_modules/nested/index.js'),
 | |
|             'NestedDependencyClass', isNamedClassDeclaration);
 | |
| 
 | |
|         expect(host.isInScope(nestedDepClass)).toBe(false);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| });
 | |
| 
 | |
| class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unknown> {
 | |
|   readonly name = DetectDecoratorHandler.name;
 | |
| 
 | |
|   constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
 | |
| 
 | |
|   detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
 | |
|     if (decorators === null) {
 | |
|       return undefined;
 | |
|     }
 | |
|     const decorator = decorators.find(decorator => decorator.name === this.decorator);
 | |
|     if (decorator === undefined) {
 | |
|       return undefined;
 | |
|     }
 | |
|     return {trigger: node, decorator, metadata: {}};
 | |
|   }
 | |
| 
 | |
|   analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
 | |
|     return {};
 | |
|   }
 | |
| 
 | |
|   compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
 | |
|     return [];
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, unknown> {
 | |
|   readonly name = DiagnosticProducingHandler.name;
 | |
|   readonly precedence = HandlerPrecedence.PRIMARY;
 | |
| 
 | |
|   detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
 | |
|     const decorator = decorators !== null ? decorators[0] : null;
 | |
|     return {trigger: node, decorator, metadata: {}};
 | |
|   }
 | |
| 
 | |
|   analyze(node: ClassDeclaration): AnalysisOutput<any> {
 | |
|     return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
 | |
|   }
 | |
| 
 | |
|   compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
 | |
|     return [];
 | |
|   }
 | |
| }
 |