Alex Rickabaugh 3cf1b62722 refactor(ivy): extract import rewriting into a separate interface (#27998)
Currently the ImportManager class handles various rewriting actions of
imports when compiling @angular/core. This is required as code compiled
within @angular/core cannot import from '@angular/core'. To work around
this, imports are rewritten to get core symbols from a particular file,
r3_symbols.ts.

In this refactoring, this rewriting logic is moved out of the ImportManager
and put behind an interface, ImportRewriter. There are three implementers
of the interface:

* NoopImportRewriter, used for compiling all non-core packages.
* R3SymbolsImportRewriter, used when ngtsc compiles @angular/core.
* NgccFlatImportRewriter, used when ngcc compiles @angular/core (special
  logic is needed because ngcc has to rewrite imports in flat bundles
  differently than in non-flat bundles).

This is a precursor to using this rewriting logic in other contexts besides
the ImportManager.

PR Close #27998
2019-01-10 10:46:32 -08:00

96 lines
3.0 KiB
TypeScript

/**
* @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
*/
import * as ts from 'typescript';
import {NoopImportRewriter} from '../../imports';
import {TypeScriptReflectionHost} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {ImportManager, translateStatement} from '../../translator';
import {generateSetClassMetadataCall} from '../src/metadata';
const CORE = {
name: 'node_modules/@angular/core/index.d.ts',
contents: `
export declare function Input(...args: any[]): any;
export declare function Inject(...args: any[]): any;
export declare function Component(...args: any[]): any;
export declare class Injector {}
`
};
describe('ngtsc setClassMetadata converter', () => {
it('should convert decorated class metadata', () => {
const res = compileAndPrint(`
import {Component} from '@angular/core';
@Component('metadata') class Target {}
`);
expect(res).toEqual(
`/*@__PURE__*/ i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null);`);
});
it('should convert decorated class constructor parameter metadata', () => {
const res = compileAndPrint(`
import {Component, Inject, Injector} from '@angular/core';
const FOO = 'foo';
@Component('metadata') class Target {
constructor(@Inject(FOO) foo: any, bar: Injector) {}
}
`);
expect(res).toContain(
`function () { return [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: Injector }]; }, null);`);
});
it('should convert decorated field metadata', () => {
const res = compileAndPrint(`
import {Component, Input} from '@angular/core';
@Component('metadata') class Target {
@Input() foo: string;
@Input('value') bar: string;
notDecorated: string;
}
`);
expect(res).toContain(`{ foo: [{ type: Input }], bar: [{ type: Input, args: ['value'] }] })`);
});
it('should not convert non-angular decorators to metadata', () => {
const res = compileAndPrint(`
declare function NotAComponent(...args: any[]): any;
@NotAComponent('metadata') class Target {}
`);
expect(res).toBe('');
});
});
function compileAndPrint(contents: string): string {
const {program} = makeProgram([
CORE, {
name: 'index.ts',
contents,
}
]);
const host = new TypeScriptReflectionHost(program.getTypeChecker());
const target = getDeclaration(program, 'index.ts', 'Target', ts.isClassDeclaration);
const call = generateSetClassMetadataCall(target, host, false);
if (call === null) {
return '';
}
const sf = program.getSourceFile('index.ts') !;
const im = new ImportManager(new NoopImportRewriter(), 'i');
const tsStatement = translateStatement(call, im);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tsStatement, sf);
return res.replace(/\s+/g, ' ');
}