Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/compiler-cli/src/compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost {
assumeFileExists(fileName: string): void;
}

export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }

export class CompilerHost implements AotCompilerHost {
protected metadataCollector = new MetadataCollector();
private isGenDirChildOfRootDir: boolean;
protected basePath: string;
private genDir: string;
Expand All @@ -39,7 +40,8 @@ export class CompilerHost implements AotCompilerHost {

constructor(
protected program: ts.Program, protected options: AngularCompilerOptions,
protected context: CompilerHostContext, collectorOptions?: CollectorOptions) {
protected context: CompilerHostContext, collectorOptions?: CollectorOptions,
protected metadataProvider: MetadataProvider = new MetadataCollector()) {
// normalize the path so that it never ends with '/'.
this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/');
this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/');
Expand Down Expand Up @@ -206,7 +208,7 @@ export class CompilerHost implements AotCompilerHost {
}

const sf = this.getSourceFile(filePath);
const metadata = this.metadataCollector.getMetadata(sf);
const metadata = this.metadataProvider.getMetadata(sf);
return metadata ? [metadata] : [];
}

Expand Down Expand Up @@ -245,7 +247,7 @@ export class CompilerHost implements AotCompilerHost {
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}

const exports = this.metadataCollector.getMetadata(this.getSourceFile(dtsFilePath));
const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath));
if (exports) {
for (let prop in exports.metadata) {
if (!v3Metadata.metadata[prop]) {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/path_mapped_compiler_host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class PathMappedCompilerHost extends CompilerHost {
} else {
const sf = this.getSourceFile(rootedPath);
sf.fileName = sf.fileName;
const metadata = this.metadataCollector.getMetadata(sf);
const metadata = this.metadataProvider.getMetadata(sf);
return metadata ? [metadata] : [];
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-cli/src/transformers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions {

// Whether to enable support for <template> and the template attribute (true by default)
enableLegacyTemplate?: boolean;

// Whether to enable lowering expressions lambdas and expressions in a reference value
// position.
disableExpressionLowering?: boolean;
}

export interface ModuleFilenameResolver {
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler-cli/src/transformers/entry_points.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @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 {CompilerHost, CompilerOptions, Program} from './api';
Expand Down
177 changes: 177 additions & 0 deletions packages/compiler-cli/src/transformers/lower_expressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* @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 {CollectorOptions, MetadataCollector, MetadataValue, ModuleMetadata} from '@angular/tsc-wrapped';
import * as ts from 'typescript';

export interface LoweringRequest {
kind: ts.SyntaxKind;
location: number;
end: number;
name: string;
}

export type RequestLocationMap = Map<number, LoweringRequest>;

interface Declaration {
name: string;
node: ts.Node;
}

interface DeclarationInsert {
declarations: Declaration[];
priorTo: ts.Node;
}

function toMap<T, K>(items: T[], select: (item: T) => K): Map<K, T> {
return new Map(items.map<[K, T]>(i => [select(i), i]));
}

function transformSourceFile(
sourceFile: ts.SourceFile, requests: RequestLocationMap,
context: ts.TransformationContext): ts.SourceFile {
const inserts: DeclarationInsert[] = [];

// Calculate the range of intersting locations. The transform will only visit nodes in this
// range to improve the performance on large files.
const locations = Array.from(requests.keys());
const min = Math.min(...locations);
const max = Math.max(...locations);

function visitSourceFile(sourceFile: ts.SourceFile): ts.SourceFile {
function topLevelStatement(node: ts.Node): ts.Node {
const declarations: Declaration[] = [];

function visitNode(node: ts.Node): ts.Node {
const nodeRequest = requests.get(node.pos);
if (nodeRequest && nodeRequest.kind == node.kind && nodeRequest.end == node.end) {
// This node is requested to be rewritten as a reference to the exported name.
// Record that the node needs to be moved to an exported variable with the given name
const name = nodeRequest.name;
declarations.push({name, node});
return ts.createIdentifier(name);
}
if (node.pos <= max && node.end >= min) return ts.visitEachChild(node, visitNode, context);
return node;
}

const result = ts.visitEachChild(node, visitNode, context);

if (declarations.length) {
inserts.push({priorTo: result, declarations})
}
return result;
}

const traversedSource = ts.visitEachChild(sourceFile, topLevelStatement, context);
if (inserts.length) {
// Insert the declarations before the rewritten statement that references them.
const insertMap = toMap(inserts, i => i.priorTo);
const newStatements: ts.Statement[] = [...traversedSource.statements];
for (let i = newStatements.length; i >= 0; i--) {
const statement = newStatements[i];
const insert = insertMap.get(statement);
if (insert) {
const declarations = insert.declarations.map(
i => ts.createVariableDeclaration(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createConst ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to be const by adding a const modifier to the declaration list.

i.name, /* type */ undefined, i.node as ts.Expression));
const statement = ts.createVariableStatement(
/* modifiers */ undefined,
ts.createVariableDeclarationList(declarations, ts.NodeFlags.Const));
newStatements.splice(i, 0, statement);
}
}

// Insert an exports clause to export the declarations
newStatements.push(ts.createExportDeclaration(
/* decorators */ undefined,
/* modifiers */ undefined,
ts.createNamedExports(
inserts
.reduce(
(accumulator, insert) => [...accumulator, ...insert.declarations],
[] as Declaration[])
.map(
declaration => ts.createExportSpecifier(
/* propertyName */ undefined, declaration.name)))));
return ts.updateSourceFileNode(traversedSource, newStatements);
}
return traversedSource;
}

return visitSourceFile(sourceFile);
}

export function getExpressionLoweringTransformFactory(requestsMap: RequestsMap):
(context: ts.TransformationContext) => (sourceFile: ts.SourceFile) => ts.SourceFile {
// Return the factory
return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => {
const requests = requestsMap.getRequests(sourceFile);
if (requests && requests.size) {
return transformSourceFile(sourceFile, requests, context)
}
return sourceFile;
};
}

export interface RequestsMap { getRequests(sourceFile: ts.SourceFile): RequestLocationMap; }

interface MetadataAndLoweringRequests {
metadata: ModuleMetadata|undefined;
requests: RequestLocationMap;
}

export class LowerMetadataCache implements RequestsMap {
private collector: MetadataCollector;
private metadataCache = new Map<string, MetadataAndLoweringRequests>();

constructor(options: CollectorOptions, private strict?: boolean) {
this.collector = new MetadataCollector(options);
}

getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined {
return this.ensureMetadataAndRequests(sourceFile).metadata;
}

getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
return this.ensureMetadataAndRequests(sourceFile).requests;
}

private ensureMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
let result = this.metadataCache.get(sourceFile.fileName);
if (!result) {
result = this.getMetadataAndRequests(sourceFile);
this.metadataCache.set(sourceFile.fileName, result);
}
return result;
}

private getMetadataAndRequests(sourceFile: ts.SourceFile): MetadataAndLoweringRequests {
let identNumber = 0;
const freshIdent = () => '\u0275' + identNumber++;
const requests = new Map<number, LoweringRequest>();
const replaceNode =
(node: ts.Node) => {
const name = freshIdent();
requests.set(node.pos, {name, kind: node.kind, location: node.pos, end: node.end});
return {__symbolic: 'reference', name};
}

const substituteExpression = (value: MetadataValue, node: ts.Node): MetadataValue => {
if (node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) {
return replaceNode(node);
}
return value;
};

const metadata = this.collector.getMetadata(sourceFile, this.strict, substituteExpression);

return {metadata, requests};
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @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 path from 'path';
import * as ts from 'typescript';

Expand Down
42 changes: 28 additions & 14 deletions packages/compiler-cli/src/transformers/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {TypeChecker} from '../diagnostics/check_types';

import {CompilerHost, CompilerOptions, DiagnosticCategory} from './api';
import {Diagnostic, EmitFlags, Program} from './api';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';

const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/;
Expand All @@ -35,7 +36,7 @@ class AngularCompilerProgram implements Program {
private aotCompilerHost: AotCompilerHost;
private compiler: AotCompiler;
private srcNames: string[];
private collector: MetadataCollector;
private metadataCache: LowerMetadataCache;
// Lazily initialized fields
private _analyzedModules: NgAnalyzedModules|undefined;
private _structuralDiagnostics: Diagnostic[] = [];
Expand All @@ -55,13 +56,14 @@ class AngularCompilerProgram implements Program {

this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram);
this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName);
this.aotCompilerHost = new AotCompilerHost(this.tsProgram, options, host);
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
this.aotCompilerHost = new AotCompilerHost(
this.tsProgram, options, host, /* collectorOptions */ undefined, this.metadataCache);
if (host.readResource) {
this.aotCompilerHost.loadResource = host.readResource.bind(host);
}
const {compiler} = createAotCompiler(this.aotCompilerHost, options);
this.compiler = compiler;
this.collector = new MetadataCollector({quotedNames: true});
}

// Program implementation
Expand Down Expand Up @@ -118,11 +120,9 @@ class AngularCompilerProgram implements Program {
const emitMap = new Map<string, string>();
const result = this.programWithStubs.emit(
/* targetSourceFile */ undefined,
createWriteFileCallback(emitFlags, this.host, this.collector, this.options, emitMap),
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, {
after: this.options.skipTemplateCodegen ? [] : [getAngularEmitterTransformFactory(
this.generatedFiles)]
});
createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap),
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
this.calculateTransforms());

this.generatedFiles.forEach(file => {
if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) {
Expand Down Expand Up @@ -184,6 +184,21 @@ class AngularCompilerProgram implements Program {
return this.generatedFiles && this._generatedFileDiagnostics !;
}

private calculateTransforms(): ts.CustomTransformers {
const before: ts.TransformerFactory<ts.SourceFile>[] = [];
const after: ts.TransformerFactory<ts.SourceFile>[] = [];
if (!this.options.disableExpressionLowering) {
before.push(getExpressionLoweringTransformFactory(this.metadataCache));
}
if (!this.options.skipTemplateCodegen) {
after.push(getAngularEmitterTransformFactory(this.generatedFiles));
}
const result: ts.CustomTransformers = {};
if (before.length) result.before = before;
if (after.length) result.after = after;
return result;
}

private catchAnalysisError(e: any): NgAnalyzedModules {
if (isSyntaxError(e)) {
const parserErrors = getParseErrors(e);
Expand Down Expand Up @@ -257,8 +272,7 @@ export function createProgram(
}

function writeMetadata(
emitFilePath: string, sourceFile: ts.SourceFile, collector: MetadataCollector,
ngOptions: CompilerOptions) {
emitFilePath: string, sourceFile: ts.SourceFile, metadataCache: LowerMetadataCache) {
if (/\.js$/.test(emitFilePath)) {
const path = emitFilePath.replace(/\.js$/, '.metadata.json');

Expand All @@ -271,7 +285,7 @@ function writeMetadata(
collectableFile = (collectableFile as any).original;
}

const metadata = collector.getMetadata(collectableFile, !!ngOptions.strictMetadataEmit);
const metadata = metadataCache.getMetadata(collectableFile);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
writeFileSync(path, metadataText, {encoding: 'utf-8'});
Expand All @@ -280,8 +294,8 @@ function writeMetadata(
}

function createWriteFileCallback(
emitFlags: EmitFlags, host: ts.CompilerHost, collector: MetadataCollector,
ngOptions: CompilerOptions, emitMap: Map<string, string>) {
emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache,
emitMap: Map<string, string>) {
const withMetadata =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
Expand All @@ -291,7 +305,7 @@ function createWriteFileCallback(
}
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
emitMap.set(sourceFiles[0].fileName, fileName);
writeMetadata(fileName, sourceFiles[0], collector, ngOptions);
writeMetadata(fileName, sourceFiles[0], metadataCache);
}
};
const withoutMetadata =
Expand Down
Loading