2018-07-16 10:23:37 +01:00
|
|
|
/**
|
|
|
|
|
* @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
|
|
|
|
|
*/
|
2018-07-27 22:36:54 +03:00
|
|
|
import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
|
|
|
|
|
import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
|
2018-07-17 10:43:42 +01:00
|
|
|
import {readFileSync, statSync} from 'fs';
|
2018-07-27 22:36:54 +03:00
|
|
|
import MagicString from 'magic-string';
|
2018-10-04 12:19:11 +01:00
|
|
|
import {basename, dirname, relative, resolve} from 'canonical-path';
|
2018-07-27 22:36:54 +03:00
|
|
|
import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
|
2018-07-16 10:23:37 +01:00
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
|
|
|
|
|
import {Decorator} from '../../../ngtsc/host';
|
2018-10-16 08:56:54 +01:00
|
|
|
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
|
|
|
|
|
import {translateStatement, translateType} from '../../../ngtsc/translator';
|
2018-09-26 20:53:45 +01:00
|
|
|
import {NgccImportManager} from './ngcc_import_manager';
|
2018-10-16 08:56:54 +01:00
|
|
|
import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
|
2018-10-04 12:19:11 +01:00
|
|
|
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
|
|
|
|
|
import {IMPORT_PREFIX} from '../constants';
|
|
|
|
|
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
2018-07-16 10:23:37 +01:00
|
|
|
|
|
|
|
|
interface SourceMapInfo {
|
|
|
|
|
source: string;
|
|
|
|
|
map: SourceMapConverter|null;
|
|
|
|
|
isInline: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Information about a file that has been rendered.
|
|
|
|
|
*/
|
|
|
|
|
export interface FileInfo {
|
|
|
|
|
/**
|
|
|
|
|
* Path to where the file should be written.
|
|
|
|
|
*/
|
|
|
|
|
path: string;
|
|
|
|
|
/**
|
|
|
|
|
* The contents of the file to be be written.
|
|
|
|
|
*/
|
|
|
|
|
contents: string;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
interface DtsClassInfo {
|
|
|
|
|
dtsDeclaration: ts.Declaration;
|
|
|
|
|
compilation: CompileResult[];
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-16 10:23:37 +01:00
|
|
|
/**
|
2018-07-27 22:36:54 +03:00
|
|
|
* A base-class for rendering an `AnalyzedFile`.
|
|
|
|
|
*
|
|
|
|
|
* Package formats have output files that must be rendered differently. Concrete sub-classes must
|
|
|
|
|
* implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods.
|
2018-07-16 10:23:37 +01:00
|
|
|
*/
|
|
|
|
|
export abstract class Renderer {
|
2018-10-03 16:59:32 +01:00
|
|
|
constructor(
|
|
|
|
|
protected host: NgccReflectionHost, protected isCore: boolean,
|
2018-10-04 12:19:11 +01:00
|
|
|
protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string,
|
2018-10-16 08:56:54 +01:00
|
|
|
protected targetPath: string) {}
|
2018-10-04 12:19:11 +01:00
|
|
|
|
|
|
|
|
renderProgram(
|
|
|
|
|
program: ts.Program, decorationAnalyses: DecorationAnalyses,
|
|
|
|
|
switchMarkerAnalyses: SwitchMarkerAnalyses): FileInfo[] {
|
|
|
|
|
const renderedFiles: FileInfo[] = [];
|
2018-10-10 17:52:55 +01:00
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
// Transform the source files.
|
2018-10-04 12:19:11 +01:00
|
|
|
program.getSourceFiles().map(sourceFile => {
|
2018-10-16 08:56:54 +01:00
|
|
|
const compiledFile = decorationAnalyses.get(sourceFile);
|
2018-10-04 12:19:11 +01:00
|
|
|
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
|
|
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
if (compiledFile || switchMarkerAnalysis) {
|
|
|
|
|
renderedFiles.push(...this.renderFile(sourceFile, compiledFile, switchMarkerAnalysis));
|
2018-10-10 17:52:55 +01:00
|
|
|
}
|
2018-10-04 12:19:11 +01:00
|
|
|
});
|
2018-10-16 08:56:54 +01:00
|
|
|
|
|
|
|
|
// Transform the .d.ts files
|
|
|
|
|
const dtsFiles = this.getTypingsFilesToRender(decorationAnalyses);
|
|
|
|
|
dtsFiles.forEach((classes, file) => renderedFiles.push(...this.renderDtsFile(file, classes)));
|
|
|
|
|
|
2018-10-04 12:19:11 +01:00
|
|
|
return renderedFiles;
|
|
|
|
|
}
|
2018-07-27 22:36:54 +03:00
|
|
|
|
2018-07-16 10:23:37 +01:00
|
|
|
/**
|
|
|
|
|
* Render the source code and source-map for an Analyzed file.
|
2018-10-16 08:56:54 +01:00
|
|
|
* @param compiledFile The analyzed file to render.
|
2018-07-16 10:23:37 +01:00
|
|
|
* @param targetPath The absolute path where the rendered file will be written.
|
|
|
|
|
*/
|
2018-10-04 12:19:11 +01:00
|
|
|
renderFile(
|
2018-10-16 08:56:54 +01:00
|
|
|
sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
|
|
|
|
|
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined): FileInfo[] {
|
2018-10-04 12:19:11 +01:00
|
|
|
const input = this.extractSourceMap(sourceFile);
|
2018-07-16 10:23:37 +01:00
|
|
|
const outputText = new MagicString(input.source);
|
|
|
|
|
|
2018-10-04 12:19:11 +01:00
|
|
|
if (switchMarkerAnalysis) {
|
|
|
|
|
this.rewriteSwitchableDeclarations(
|
|
|
|
|
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
|
|
|
|
|
}
|
2018-07-16 10:23:37 +01:00
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
if (compiledFile) {
|
2018-10-04 12:19:11 +01:00
|
|
|
const importManager =
|
|
|
|
|
new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX);
|
|
|
|
|
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
|
|
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
compiledFile.compiledClasses.forEach(clazz => {
|
|
|
|
|
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
|
2018-10-04 12:19:11 +01:00
|
|
|
this.addDefinitions(outputText, clazz, renderedDefinition);
|
|
|
|
|
this.trackDecorators(clazz.decorators, decoratorsToRemove);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.addConstants(
|
|
|
|
|
outputText,
|
2018-10-16 08:56:54 +01:00
|
|
|
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
|
|
|
|
|
compiledFile.sourceFile);
|
2018-10-04 12:19:11 +01:00
|
|
|
|
|
|
|
|
this.addImports(
|
2018-10-16 08:56:54 +01:00
|
|
|
outputText,
|
|
|
|
|
importManager.getAllImports(compiledFile.sourceFile.fileName, this.rewriteCoreImportsTo));
|
2018-10-04 12:19:11 +01:00
|
|
|
|
|
|
|
|
// TODO: remove contructor param metadata and property decorators (we need info from the
|
|
|
|
|
// handlers to do this)
|
|
|
|
|
this.removeDecorators(outputText, decoratorsToRemove);
|
|
|
|
|
}
|
2018-08-17 07:50:45 +01:00
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
return this.renderSourceAndMap(sourceFile, input, outputText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderDtsFile(dtsFile: ts.SourceFile, dtsClasses: DtsClassInfo[]): FileInfo[] {
|
|
|
|
|
const input = this.extractSourceMap(dtsFile);
|
|
|
|
|
const outputText = new MagicString(input.source);
|
|
|
|
|
const importManager = new NgccImportManager(false, this.isCore, IMPORT_PREFIX);
|
|
|
|
|
|
|
|
|
|
dtsClasses.forEach(dtsClass => {
|
|
|
|
|
const endOfClass = dtsClass.dtsDeclaration.getEnd();
|
|
|
|
|
dtsClass.compilation.forEach(declaration => {
|
|
|
|
|
const type = translateType(declaration.type, importManager);
|
|
|
|
|
const newStatement = ` static ${declaration.name}: ${type};\n`;
|
|
|
|
|
outputText.appendRight(endOfClass - 1, newStatement);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.addImports(
|
|
|
|
|
outputText, importManager.getAllImports(dtsFile.fileName, this.rewriteCoreImportsTo));
|
|
|
|
|
|
|
|
|
|
return this.renderSourceAndMap(dtsFile, input, outputText);
|
2018-07-16 10:23:37 +01:00
|
|
|
}
|
|
|
|
|
|
2018-06-27 08:41:11 -07:00
|
|
|
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
|
|
|
|
|
void;
|
2018-07-16 10:23:37 +01:00
|
|
|
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void;
|
|
|
|
|
protected abstract addDefinitions(
|
2018-10-16 08:56:54 +01:00
|
|
|
output: MagicString, compiledClass: CompiledClass, definitions: string): void;
|
2018-07-16 10:23:37 +01:00
|
|
|
protected abstract removeDecorators(
|
|
|
|
|
output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void;
|
2018-08-17 07:50:45 +01:00
|
|
|
protected abstract rewriteSwitchableDeclarations(
|
2018-10-04 12:19:11 +01:00
|
|
|
outputText: MagicString, sourceFile: ts.SourceFile,
|
|
|
|
|
declarations: SwitchableVariableDeclaration[]): void;
|
2018-07-16 10:23:37 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the decorator nodes that are to be removed to a map
|
|
|
|
|
* So that we can tell if we should remove the entire decorator property
|
|
|
|
|
*/
|
|
|
|
|
protected trackDecorators(decorators: Decorator[], decoratorsToRemove: Map<ts.Node, ts.Node[]>):
|
|
|
|
|
void {
|
|
|
|
|
decorators.forEach(dec => {
|
|
|
|
|
const decoratorArray = dec.node.parent !;
|
|
|
|
|
if (!decoratorsToRemove.has(decoratorArray)) {
|
|
|
|
|
decoratorsToRemove.set(decoratorArray, [dec.node]);
|
|
|
|
|
} else {
|
|
|
|
|
decoratorsToRemove.get(decoratorArray) !.push(dec.node);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the map from the source (note whether it is inline or external)
|
|
|
|
|
*/
|
|
|
|
|
protected extractSourceMap(file: ts.SourceFile): SourceMapInfo {
|
|
|
|
|
const inline = commentRegex.test(file.text);
|
|
|
|
|
const external = mapFileCommentRegex.test(file.text);
|
|
|
|
|
|
|
|
|
|
if (inline) {
|
|
|
|
|
const inlineSourceMap = fromSource(file.text);
|
|
|
|
|
return {
|
|
|
|
|
source: removeComments(file.text).replace(/\n\n$/, '\n'),
|
|
|
|
|
map: inlineSourceMap,
|
|
|
|
|
isInline: true,
|
|
|
|
|
};
|
|
|
|
|
} else if (external) {
|
|
|
|
|
let externalSourceMap: SourceMapConverter|null = null;
|
|
|
|
|
try {
|
|
|
|
|
externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName));
|
|
|
|
|
} catch (e) {
|
2018-07-17 10:43:42 +01:00
|
|
|
if (e.code === 'ENOENT') {
|
|
|
|
|
console.warn(
|
|
|
|
|
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
|
|
|
|
|
const mapPath = file.fileName + '.map';
|
|
|
|
|
if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) {
|
|
|
|
|
console.warn(
|
|
|
|
|
`Guessing the map file name from the source file name: "${basename(mapPath)}"`);
|
|
|
|
|
try {
|
|
|
|
|
externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8')));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-16 10:23:37 +01:00
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
|
|
|
|
|
map: externalSourceMap,
|
|
|
|
|
isInline: false,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
return {source: file.text, map: null, isInline: false};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Merge the input and output source-maps, replacing the source-map comment in the output file
|
|
|
|
|
* with an appropriate source-map comment pointing to the merged source-map.
|
|
|
|
|
*/
|
|
|
|
|
protected renderSourceAndMap(
|
2018-10-16 08:56:54 +01:00
|
|
|
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
|
|
|
|
|
const outputPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
|
2018-07-16 10:23:37 +01:00
|
|
|
const outputMapPath = `${outputPath}.map`;
|
|
|
|
|
const outputMap = output.generateMap({
|
2018-10-04 12:19:11 +01:00
|
|
|
source: sourceFile.fileName,
|
2018-07-16 10:23:37 +01:00
|
|
|
includeContent: true,
|
|
|
|
|
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
|
|
|
|
|
// the merge algorithm.
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// we must set this after generation as magic string does "manipulation" on the path
|
|
|
|
|
outputMap.file = outputPath;
|
|
|
|
|
|
|
|
|
|
const mergedMap =
|
|
|
|
|
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
|
|
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
const result: FileInfo[] = [];
|
2018-07-16 10:23:37 +01:00
|
|
|
if (input.isInline) {
|
2018-10-16 08:56:54 +01:00
|
|
|
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
|
2018-07-16 10:23:37 +01:00
|
|
|
} else {
|
2018-10-16 08:56:54 +01:00
|
|
|
result.push({
|
|
|
|
|
path: outputPath,
|
|
|
|
|
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
|
|
|
|
|
});
|
|
|
|
|
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
|
2018-07-16 10:23:37 +01:00
|
|
|
}
|
2018-10-16 08:56:54 +01:00
|
|
|
return result;
|
2018-07-16 10:23:37 +01:00
|
|
|
}
|
2018-10-10 17:52:55 +01:00
|
|
|
|
2018-10-16 08:56:54 +01:00
|
|
|
protected getTypingsFilesToRender(analyses: DecorationAnalyses):
|
|
|
|
|
Map<ts.SourceFile, DtsClassInfo[]> {
|
|
|
|
|
const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>();
|
|
|
|
|
analyses.forEach(compiledFile => {
|
|
|
|
|
compiledFile.compiledClasses.forEach(compiledClass => {
|
|
|
|
|
const dtsDeclaration = this.host.getDtsDeclarationOfClass(compiledClass.declaration);
|
|
|
|
|
if (dtsDeclaration) {
|
|
|
|
|
const dtsFile = dtsDeclaration.getSourceFile();
|
|
|
|
|
const classes = dtsMap.get(dtsFile) || [];
|
|
|
|
|
classes.push({dtsDeclaration, compilation: compiledClass.compilation});
|
|
|
|
|
dtsMap.set(dtsFile, classes);
|
|
|
|
|
}
|
2018-10-10 17:52:55 +01:00
|
|
|
});
|
2018-10-16 08:56:54 +01:00
|
|
|
});
|
|
|
|
|
return dtsMap;
|
2018-10-10 17:52:55 +01:00
|
|
|
}
|
2018-07-16 10:23:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Merge the two specified source-maps into a single source-map that hides the intermediate
|
|
|
|
|
* source-map.
|
|
|
|
|
* E.g. Consider these mappings:
|
|
|
|
|
*
|
|
|
|
|
* ```
|
|
|
|
|
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
|
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* this will be replaced with:
|
|
|
|
|
*
|
|
|
|
|
* ```
|
|
|
|
|
* OLD_SRC -> MERGED_MAP -> NEW_SRC
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export function mergeSourceMaps(
|
|
|
|
|
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
|
|
|
|
|
if (!oldMap) {
|
|
|
|
|
return fromObject(newMap);
|
|
|
|
|
}
|
|
|
|
|
const oldMapConsumer = new SourceMapConsumer(oldMap);
|
|
|
|
|
const newMapConsumer = new SourceMapConsumer(newMap);
|
|
|
|
|
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
|
|
|
|
|
mergedMapGenerator.applySourceMap(oldMapConsumer);
|
|
|
|
|
const merged = fromJSON(mergedMapGenerator.toString());
|
|
|
|
|
return merged;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 08:41:11 -07:00
|
|
|
/**
|
|
|
|
|
* Render the constant pool as source code for the given class.
|
|
|
|
|
*/
|
|
|
|
|
export function renderConstantPool(
|
2018-10-03 16:59:32 +01:00
|
|
|
sourceFile: ts.SourceFile, constantPool: ConstantPool, imports: NgccImportManager): string {
|
2018-06-27 08:41:11 -07:00
|
|
|
const printer = ts.createPrinter();
|
|
|
|
|
return constantPool.statements.map(stmt => translateStatement(stmt, imports))
|
|
|
|
|
.map(stmt => printer.printNode(ts.EmitHint.Unspecified, stmt, sourceFile))
|
|
|
|
|
.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-16 10:23:37 +01:00
|
|
|
/**
|
|
|
|
|
* Render the definitions as source code for the given class.
|
|
|
|
|
* @param sourceFile The file containing the class to process.
|
|
|
|
|
* @param clazz The class whose definitions are to be rendered.
|
|
|
|
|
* @param compilation The results of analyzing the class - this is used to generate the rendered
|
|
|
|
|
* definitions.
|
|
|
|
|
* @param imports An object that tracks the imports that are needed by the rendered definitions.
|
|
|
|
|
*/
|
|
|
|
|
export function renderDefinitions(
|
2018-10-16 08:56:54 +01:00
|
|
|
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: NgccImportManager): string {
|
2018-07-16 10:23:37 +01:00
|
|
|
const printer = ts.createPrinter();
|
2018-10-16 08:56:54 +01:00
|
|
|
const name = (compiledClass.declaration as ts.NamedDeclaration).name !;
|
2018-07-16 10:23:37 +01:00
|
|
|
const definitions =
|
2018-10-16 08:56:54 +01:00
|
|
|
compiledClass.compilation
|
2018-07-16 10:23:37 +01:00
|
|
|
.map(
|
|
|
|
|
c => c.statements.map(statement => translateStatement(statement, imports))
|
|
|
|
|
.concat(translateStatement(
|
|
|
|
|
createAssignmentStatement(name, c.name, c.initializer), imports))
|
|
|
|
|
.map(
|
|
|
|
|
statement =>
|
|
|
|
|
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
|
|
|
|
|
.join('\n'))
|
|
|
|
|
.join('\n');
|
|
|
|
|
return definitions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create an Angular AST statement node that contains the assignment of the
|
|
|
|
|
* compiled decorator to be applied to the class.
|
|
|
|
|
* @param analyzedClass The info about the class whose statement we want to create.
|
|
|
|
|
*/
|
|
|
|
|
function createAssignmentStatement(
|
|
|
|
|
receiverName: ts.DeclarationName, propName: string, initializer: Expression): Statement {
|
|
|
|
|
const receiver = new WrappedNodeExpr(receiverName);
|
|
|
|
|
return new WritePropExpr(receiver, propName, initializer).toStmt();
|
|
|
|
|
}
|