Skip to content

Commit 5b32aa4

Browse files
petebacondarwinIgorMinar
authored andcommitted
feat(ivy): implement esm2015 and esm5 ngcc file renderers (angular#24897)
PR Close angular#24897
1 parent 844d510 commit 5b32aa4

File tree

6 files changed

+916
-0
lines changed

6 files changed

+916
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
import MagicString from 'magic-string';
10+
import {NgccReflectionHost} from '../host/ngcc_host';
11+
import {AnalyzedClass} from '../analyzer';
12+
import {Renderer} from './renderer';
13+
14+
export class Esm2015Renderer extends Renderer {
15+
constructor(protected host: NgccReflectionHost) { super(); }
16+
17+
/**
18+
* Add the imports at the top of the file
19+
*/
20+
addImports(output: MagicString, imports: {name: string; as: string;}[]): void {
21+
// The imports get inserted at the very top of the file.
22+
imports.forEach(i => { output.appendLeft(0, `import * as ${i.as} from '${i.name}';\n`); });
23+
}
24+
25+
/**
26+
* Add the definitions to each decorated class
27+
*/
28+
addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void {
29+
const classSymbol = this.host.getClassSymbol(analyzedClass.declaration);
30+
if (!classSymbol) {
31+
throw new Error(`Analyzed class does not have a valid symbol: ${analyzedClass.name}`);
32+
}
33+
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
34+
output.appendLeft(insertionPoint, '\n' + definitions);
35+
}
36+
37+
/**
38+
* Remove static decorator properties from classes
39+
*/
40+
removeDecorators(output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void {
41+
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
42+
if (ts.isArrayLiteralExpression(containerNode)) {
43+
const items = containerNode.elements;
44+
if (items.length === nodesToRemove.length) {
45+
// remove any trailing semi-colon
46+
const end = (output.slice(containerNode.getEnd(), containerNode.getEnd() + 1) === ';') ?
47+
containerNode.getEnd() + 1 :
48+
containerNode.getEnd();
49+
output.remove(containerNode.parent !.getFullStart(), end);
50+
} else {
51+
nodesToRemove.forEach(node => {
52+
// remove any trailing comma
53+
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
54+
node.getEnd() + 1 :
55+
node.getEnd();
56+
output.remove(node.getFullStart(), end);
57+
});
58+
}
59+
}
60+
});
61+
}
62+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import * as ts from 'typescript';
9+
import MagicString from 'magic-string';
10+
import {NgccReflectionHost} from '../host/ngcc_host';
11+
import {AnalyzedClass, AnalyzedFile} from '../analyzer';
12+
import {Esm2015Renderer} from './esm2015_renderer';
13+
14+
export class Esm5Renderer extends Esm2015Renderer {
15+
constructor(host: NgccReflectionHost) { super(host); }
16+
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {dirname} from 'path';
9+
import * as ts from 'typescript';
10+
11+
import MagicString from 'magic-string';
12+
import {commentRegex, mapFileCommentRegex, fromJSON, fromSource, fromMapFileSource, fromObject, generateMapFileComment, removeComments, removeMapFileComments, SourceMapConverter} from 'convert-source-map';
13+
import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
14+
import {Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
15+
import {AnalyzedClass, AnalyzedFile} from '../analyzer';
16+
import {Decorator} from '../../../ngtsc/host';
17+
import {ImportManager, translateStatement} from '../../../ngtsc/transform/src/translator';
18+
19+
interface SourceMapInfo {
20+
source: string;
21+
map: SourceMapConverter|null;
22+
isInline: boolean;
23+
}
24+
25+
/**
26+
* The results of rendering an analyzed file.
27+
*/
28+
export interface RenderResult {
29+
/**
30+
* The file that has been rendered.
31+
*/
32+
file: AnalyzedFile;
33+
/**
34+
* The rendered source file.
35+
*/
36+
source: FileInfo;
37+
/**
38+
* The rendered source map file.
39+
*/
40+
map: FileInfo|null;
41+
}
42+
43+
/**
44+
* Information about a file that has been rendered.
45+
*/
46+
export interface FileInfo {
47+
/**
48+
* Path to where the file should be written.
49+
*/
50+
path: string;
51+
/**
52+
* The contents of the file to be be written.
53+
*/
54+
contents: string;
55+
}
56+
57+
/**
58+
* A base-class for rendering an `AnalyzedClass`.
59+
* Package formats have output files that must be rendered differently,
60+
* Concrete sub-classes must implement the `addImports`, `addDefinitions` and
61+
* `removeDecorators` abstract methods.
62+
*/
63+
export abstract class Renderer {
64+
/**
65+
* Render the source code and source-map for an Analyzed file.
66+
* @param file The analyzed file to render.
67+
* @param targetPath The absolute path where the rendered file will be written.
68+
*/
69+
renderFile(file: AnalyzedFile, targetPath: string): RenderResult {
70+
const importManager = new ImportManager(false, 'ɵngcc');
71+
const input = this.extractSourceMap(file.sourceFile);
72+
73+
const outputText = new MagicString(input.source);
74+
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
75+
76+
file.analyzedClasses.forEach(clazz => {
77+
const renderedDefinition = renderDefinitions(file.sourceFile, clazz, importManager);
78+
this.addDefinitions(outputText, clazz, renderedDefinition);
79+
this.trackDecorators(clazz.decorators, decoratorsToRemove);
80+
});
81+
82+
this.addImports(outputText, importManager.getAllImports(file.sourceFile.fileName, null));
83+
// QUESTION: do we need to remove contructor param metadata and property decorators?
84+
this.removeDecorators(outputText, decoratorsToRemove);
85+
86+
return this.renderSourceAndMap(file, input, outputText, targetPath);
87+
}
88+
89+
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void;
90+
protected abstract addDefinitions(
91+
output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void;
92+
protected abstract removeDecorators(
93+
output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void;
94+
95+
/**
96+
* Add the decorator nodes that are to be removed to a map
97+
* So that we can tell if we should remove the entire decorator property
98+
*/
99+
protected trackDecorators(decorators: Decorator[], decoratorsToRemove: Map<ts.Node, ts.Node[]>):
100+
void {
101+
decorators.forEach(dec => {
102+
const decoratorArray = dec.node.parent !;
103+
if (!decoratorsToRemove.has(decoratorArray)) {
104+
decoratorsToRemove.set(decoratorArray, [dec.node]);
105+
} else {
106+
decoratorsToRemove.get(decoratorArray) !.push(dec.node);
107+
}
108+
});
109+
}
110+
111+
/**
112+
* Get the map from the source (note whether it is inline or external)
113+
*/
114+
protected extractSourceMap(file: ts.SourceFile): SourceMapInfo {
115+
const inline = commentRegex.test(file.text);
116+
const external = mapFileCommentRegex.test(file.text);
117+
118+
if (inline) {
119+
const inlineSourceMap = fromSource(file.text);
120+
return {
121+
source: removeComments(file.text).replace(/\n\n$/, '\n'),
122+
map: inlineSourceMap,
123+
isInline: true,
124+
};
125+
} else if (external) {
126+
let externalSourceMap: SourceMapConverter|null = null;
127+
try {
128+
externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName));
129+
} catch (e) {
130+
console.warn(e);
131+
}
132+
return {
133+
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
134+
map: externalSourceMap,
135+
isInline: false,
136+
};
137+
} else {
138+
return {source: file.text, map: null, isInline: false};
139+
}
140+
}
141+
142+
/**
143+
* Merge the input and output source-maps, replacing the source-map comment in the output file
144+
* with an appropriate source-map comment pointing to the merged source-map.
145+
*/
146+
protected renderSourceAndMap(
147+
file: AnalyzedFile, input: SourceMapInfo, output: MagicString,
148+
outputPath: string): RenderResult {
149+
const outputMapPath = `${outputPath}.map`;
150+
const outputMap = output.generateMap({
151+
source: file.sourceFile.fileName,
152+
includeContent: true,
153+
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
154+
// the merge algorithm.
155+
});
156+
157+
// we must set this after generation as magic string does "manipulation" on the path
158+
outputMap.file = outputPath;
159+
160+
const mergedMap =
161+
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
162+
163+
if (input.isInline) {
164+
return {
165+
file,
166+
source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`},
167+
map: null
168+
};
169+
} else {
170+
return {
171+
file,
172+
source: {
173+
path: outputPath,
174+
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
175+
},
176+
map: {path: outputMapPath, contents: mergedMap.toJSON()}
177+
};
178+
}
179+
}
180+
}
181+
182+
/**
183+
* Merge the two specified source-maps into a single source-map that hides the intermediate
184+
* source-map.
185+
* E.g. Consider these mappings:
186+
*
187+
* ```
188+
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
189+
* ```
190+
*
191+
* this will be replaced with:
192+
*
193+
* ```
194+
* OLD_SRC -> MERGED_MAP -> NEW_SRC
195+
* ```
196+
*/
197+
export function mergeSourceMaps(
198+
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
199+
if (!oldMap) {
200+
return fromObject(newMap);
201+
}
202+
const oldMapConsumer = new SourceMapConsumer(oldMap);
203+
const newMapConsumer = new SourceMapConsumer(newMap);
204+
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
205+
mergedMapGenerator.applySourceMap(oldMapConsumer);
206+
const merged = fromJSON(mergedMapGenerator.toString());
207+
return merged;
208+
}
209+
210+
/**
211+
* Render the definitions as source code for the given class.
212+
* @param sourceFile The file containing the class to process.
213+
* @param clazz The class whose definitions are to be rendered.
214+
* @param compilation The results of analyzing the class - this is used to generate the rendered
215+
* definitions.
216+
* @param imports An object that tracks the imports that are needed by the rendered definitions.
217+
*/
218+
export function renderDefinitions(
219+
sourceFile: ts.SourceFile, analyzedClass: AnalyzedClass, imports: ImportManager): string {
220+
const printer = ts.createPrinter();
221+
const name = (analyzedClass.declaration as ts.NamedDeclaration).name !;
222+
const definitions =
223+
analyzedClass.compilation
224+
.map(
225+
c => c.statements.map(statement => translateStatement(statement, imports))
226+
.concat(translateStatement(
227+
createAssignmentStatement(name, c.name, c.initializer), imports))
228+
.map(
229+
statement =>
230+
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
231+
.join('\n'))
232+
.join('\n');
233+
return definitions;
234+
}
235+
236+
/**
237+
* Create an Angular AST statement node that contains the assignment of the
238+
* compiled decorator to be applied to the class.
239+
* @param analyzedClass The info about the class whose statement we want to create.
240+
*/
241+
function createAssignmentStatement(
242+
receiverName: ts.DeclarationName, propName: string, initializer: Expression): Statement {
243+
const receiver = new WrappedNodeExpr(receiverName);
244+
return new WritePropExpr(receiver, propName, initializer).toStmt();
245+
}

0 commit comments

Comments
 (0)