Alex Rickabaugh 24b2f1da2b refactor(ivy): introduce the 'core' package and split apart NgtscProgram (#34887)
Previously, NgtscProgram lived in the main @angular/compiler-cli package
alongside the legacy View Engine compiler. As a result, the main package
depended on all of the ngtsc internal packages, and a significant portion of
ngtsc logic lived in NgtscProgram.

This commit refactors NgtscProgram and moves the main logic of compilation
into a new 'core' package. The new package defines a new API which enables
implementers of TypeScript compilers (compilers built using the TS API) to
support Angular transpilation as well. It involves a new NgCompiler type
which takes a ts.Program and performs Angular analysis and transformations,
as well as an NgCompilerHost which wraps an input ts.CompilerHost and adds
any extra Angular files.

Together, these two classes are used to implement a new NgtscProgram which
adapts the legacy api.Program interface used by the View Engine compiler
onto operations on the new types. The new NgtscProgram implementation is
significantly smaller and easier to reason about.

The new NgCompilerHost replaces the previous GeneratedShimsHostWrapper which
lived in the 'shims' package.

A new 'resource' package is added to support the HostResourceLoader which
previously lived in the outer compiler package.

As a result of the refactoring, the dependencies of the outer
@angular/compiler-cli package on ngtsc internal packages are significantly
trimmed.

This refactoring was driven by the desire to build a plugin interface to the
compiler so that tsc_wrapped (another consumer of the TS compiler APIs) can
perform Angular transpilation on user request.

PR Close #34887
2020-01-24 08:59:59 -08:00

292 lines
10 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 {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {AliasingHost, Reference, UnifiedModulesAliasingHost} from '../../imports';
import {DtsMetadataReader} from '../../metadata';
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {makeProgram} from '../../testing';
import {ExportScope} from '../src/api';
import {MetadataDtsModuleScopeResolver} from '../src/dependency';
const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/;
const testHost: UnifiedModulesHost = {
fileNameToModuleName: function(imported: string): string {
const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !;
return 'root/' + res[1];
}
};
/**
* Simple metadata types are added to the top of each testing file, for convenience.
*/
const PROLOG = `
export declare type ModuleMeta<A, B, C, D> = never;
export declare type ComponentMeta<A, B, C, D, E, F> = never;
export declare type DirectiveMeta<A, B, C, D, E, F> = never;
export declare type PipeMeta<A, B> = never;
`;
/**
* Construct the testing environment with a given set of absolute modules and their contents.
*
* This returns both the `MetadataDtsModuleScopeResolver` and a `refs` object which can be
* destructured to retrieve references to specific declared classes.
*/
function makeTestEnv(
modules: {[module: string]: string}, aliasGenerator: AliasingHost | null = null): {
refs: {[name: string]: Reference<ClassDeclaration>},
resolver: MetadataDtsModuleScopeResolver,
} {
// Map the modules object to an array of files for `makeProgram`.
const files = Object.keys(modules).map(moduleName => {
return {
name: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`),
contents: PROLOG + (modules as any)[moduleName],
};
});
const {program} = makeProgram(files);
const checker = program.getTypeChecker();
const reflector = new TypeScriptReflectionHost(checker);
const resolver =
new MetadataDtsModuleScopeResolver(new DtsMetadataReader(checker, reflector), aliasGenerator);
// Resolver for the refs object.
const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => {
for (const sf of program.getSourceFiles()) {
const symbol = checker.getSymbolAtLocation(sf) !;
const exportedSymbol = symbol.exports !.get(name as ts.__String);
if (exportedSymbol !== undefined) {
const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration;
const specifier = MODULE_FROM_NODE_MODULES_PATH.exec(sf.fileName) ![1];
return new Reference(decl, {specifier, resolutionContext: sf.fileName});
}
}
throw new Error('Class not found: ' + name);
};
return {
resolver,
refs: new Proxy({}, {get}),
};
}
runInEachFileSystem(() => {
describe('MetadataDtsModuleScopeResolver', () => {
it('should produce an accurate scope for a basic NgModule', () => {
const {resolver, refs} = makeTestEnv({
'test': `
export declare class Dir {
static ɵdir: DirectiveMeta<Dir, '[dir]', ['exportAs'], {'input': 'input2'},
{'output': 'output2'}, ['query']>;
}
export declare class Module {
static ɵmod: ModuleMeta<Module, [typeof Dir], never, [typeof Dir]>;
}
`
});
const {Dir, Module} = refs;
const scope = resolver.resolve(Module) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
it('should produce an accurate scope when a module is exported', () => {
const {resolver, refs} = makeTestEnv({
'test': `
export declare class Dir {
static ɵdir: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
}
export declare class ModuleA {
static ɵmod: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
}
export declare class ModuleB {
static ɵmod: ModuleMeta<ModuleB, never, never, [typeof ModuleA]>;
}
`
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
});
it('should resolve correctly across modules', () => {
const {resolver, refs} = makeTestEnv({
'declaration': `
export declare class Dir {
static ɵdir: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
}
export declare class ModuleA {
static ɵmod: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
}
`,
'exported': `
import * as d from 'declaration';
export declare class ModuleB {
static ɵmod: ModuleMeta<ModuleB, never, never, [typeof d.ModuleA]>;
}
`
});
const {Dir, ModuleB} = refs;
const scope = resolver.resolve(ModuleB) !;
expect(scopeToRefs(scope)).toEqual([Dir]);
// Explicitly verify that the directive has the correct owning module.
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
});
it('should write correct aliases for deep dependencies', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ɵdir: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ɵmod: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
static ɵdir: DirectiveMeta<MiddleDir, '[middle]', never, never, never, never>;
}
export declare class MiddleModule {
static ɵmod: ModuleMeta<MiddleModule, [typeof MiddleDir], never, [typeof MiddleDir, typeof deep.DeepModule]>;
}
`,
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
static ɵdir: DirectiveMeta<ShallowDir, '[middle]', never, never, never, never>;
}
export declare class ShallowModule {
static ɵmod: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should write correct aliases for bare directives in exports', () => {
const {resolver, refs} = makeTestEnv(
{
'deep': `
export declare class DeepDir {
static ɵdir: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ɵmod: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
`,
'middle': `
import * as deep from 'deep';
export declare class MiddleDir {
static ɵdir: DirectiveMeta<MiddleDir, '[middle]', never, never, never, never>;
}
export declare class MiddleModule {
static ɵmod: ModuleMeta<MiddleModule, [typeof MiddleDir], [typeof deep.DeepModule], [typeof MiddleDir, typeof deep.DeepDir]>;
}
`,
'shallow': `
import * as middle from 'middle';
export declare class ShallowDir {
static ɵdir: DirectiveMeta<ShallowDir, '[middle]', never, never, never, never>;
}
export declare class ShallowModule {
static ɵmod: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
}
`,
},
new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$deep$$DeepDir',
});
expect(getAlias(MiddleDir)).toEqual({
moduleName: 'root/shallow',
name: 'ɵng$root$middle$$MiddleDir',
});
expect(getAlias(ShallowDir)).toBeNull();
});
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
() => {
const {resolver, refs} = makeTestEnv(
{
'module': `
export declare class DeepDir {
static ɵdir: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
}
export declare class DeepModule {
static ɵmod: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
}
export declare class DeepExportModule {
static ɵmod: ModuleMeta<DeepExportModule, never, never, [typeof DeepModule]>;
}
`,
},
new UnifiedModulesAliasingHost(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);
expect(getAlias(DeepDir)).toBeNull();
});
});
function scopeToRefs(scope: ExportScope): Reference<ClassDeclaration>[] {
const directives = scope.exported.directives.map(dir => dir.ref);
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
}
function getAlias(ref: Reference<ClassDeclaration>): ExternalReference|null {
if (ref.alias === null) {
return null;
} else {
return (ref.alias as ExternalExpr).value;
}
}
});