2019-04-28 20:48:35 +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
|
|
|
|
|
*/
|
|
|
|
|
import MagicString from 'magic-string';
|
|
|
|
|
import * as ts from 'typescript';
|
2019-06-06 20:22:32 +01:00
|
|
|
import {relative, dirname, AbsoluteFsPath, absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
|
2019-04-28 20:48:35 +01:00
|
|
|
import {Import, ImportManager} from '../../../src/ngtsc/translator';
|
|
|
|
|
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
|
2019-07-18 21:05:32 +01:00
|
|
|
import {CompiledClass} from '../analysis/types';
|
2019-04-28 20:48:35 +01:00
|
|
|
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
|
|
|
|
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
|
|
|
|
|
import {ExportInfo} from '../analysis/private_declarations_analyzer';
|
|
|
|
|
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
|
|
|
|
|
import {stripExtension} from './utils';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A RenderingFormatter that works with ECMAScript Module import and export statements.
|
|
|
|
|
*/
|
|
|
|
|
export class EsmRenderingFormatter implements RenderingFormatter {
|
|
|
|
|
constructor(protected host: NgccReflectionHost, protected isCore: boolean) {}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the imports at the top of the file, after any imports that are already there.
|
|
|
|
|
*/
|
|
|
|
|
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void {
|
|
|
|
|
const insertionPoint = this.findEndOfImports(sf);
|
|
|
|
|
const renderedImports =
|
|
|
|
|
imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join('');
|
|
|
|
|
output.appendLeft(insertionPoint, renderedImports);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the exports to the end of the file.
|
|
|
|
|
*/
|
|
|
|
|
addExports(
|
|
|
|
|
output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[],
|
|
|
|
|
importManager: ImportManager, file: ts.SourceFile): void {
|
|
|
|
|
exports.forEach(e => {
|
|
|
|
|
let exportFrom = '';
|
|
|
|
|
const isDtsFile = isDtsPath(entryPointBasePath);
|
|
|
|
|
const from = isDtsFile ? e.dtsFrom : e.from;
|
|
|
|
|
|
|
|
|
|
if (from) {
|
|
|
|
|
const basePath = stripExtension(from);
|
2019-06-06 20:22:32 +01:00
|
|
|
const relativePath = './' + relative(dirname(entryPointBasePath), basePath);
|
2019-04-28 20:48:35 +01:00
|
|
|
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// aliases should only be added in dts files as these are lost when rolling up dts file.
|
|
|
|
|
const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier;
|
|
|
|
|
const exportStr = `\nexport {${exportStatement}}${exportFrom};`;
|
|
|
|
|
output.append(exportStr);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the constants directly after the imports.
|
|
|
|
|
*/
|
|
|
|
|
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
|
|
|
|
|
if (constants === '') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const insertionPoint = this.findEndOfImports(file);
|
|
|
|
|
|
|
|
|
|
// Append the constants to the right of the insertion point, to ensure they get ordered after
|
|
|
|
|
// added imports (those are appended left to the insertion point).
|
|
|
|
|
output.appendRight(insertionPoint, '\n' + constants + '\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the definitions directly after their decorated class.
|
|
|
|
|
*/
|
|
|
|
|
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
|
|
|
|
|
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
|
|
|
|
|
if (!classSymbol) {
|
|
|
|
|
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
|
|
|
|
|
}
|
fix(ngcc): consistently use outer declaration for classes (#32539)
In ngcc's reflection hosts for compiled JS bundles, such as ESM2015,
special care needs to be taken for classes as there may be an outer
declaration (referred to as "declaration") and an inner declaration
(referred to as "implementation") for a given class. Therefore, there
will also be two `ts.Symbol`s bound per class, and ngcc needs to switch
between those declarations and symbols depending on where certain
information can be found.
Prior to this commit, the `NgccReflectionHost` interface had methods
`getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`.
These class symbols would be used to kick off compilation of components
using ngtsc, so it is important for these symbols to correspond with the
publicly visible outer declaration of the class. However, the ESM2015
reflection host used to return the `ts.Symbol` for the inner
declaration, if the class was declared as follows:
```javascript
var MyClass = class MyClass {};
```
For the above code, `Esm2015ReflectionHost.getClassSymbol` would return
the `ts.Symbol` corresponding with the `class MyClass {}` declaration,
whereas it should have corresponded with the `var MyClass` declaration.
As a consequence, no `NgModule` could be resolved for the component, so
no components/directives would be in scope for the component. This
resulted in errors during runtime.
This commit resolves the issue by introducing a `NgccClassSymbol` that
contains references to both the outer and inner `ts.Symbol`, instead of
just a single `ts.Symbol`. This avoids the unclarity of whether a
`ts.Symbol` corresponds with the outer or inner declaration.
More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw
Fixes #32078
Closes FW-1507
PR Close #32539
2019-09-03 21:26:58 +02:00
|
|
|
const insertionPoint = classSymbol.declaration.valueDeclaration !.getEnd();
|
2019-04-28 20:48:35 +01:00
|
|
|
output.appendLeft(insertionPoint, '\n' + definitions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove static decorator properties from classes.
|
|
|
|
|
*/
|
|
|
|
|
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void {
|
|
|
|
|
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
|
|
|
|
|
if (ts.isArrayLiteralExpression(containerNode)) {
|
|
|
|
|
const items = containerNode.elements;
|
|
|
|
|
if (items.length === nodesToRemove.length) {
|
|
|
|
|
// Remove the entire statement
|
|
|
|
|
const statement = findStatement(containerNode);
|
|
|
|
|
if (statement) {
|
|
|
|
|
output.remove(statement.getFullStart(), statement.getEnd());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
nodesToRemove.forEach(node => {
|
|
|
|
|
// remove any trailing comma
|
|
|
|
|
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
|
|
|
|
|
node.getEnd() + 1 :
|
|
|
|
|
node.getEnd();
|
|
|
|
|
output.remove(node.getFullStart(), end);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rewrite the the IVY switch markers to indicate we are in IVY mode.
|
|
|
|
|
*/
|
|
|
|
|
rewriteSwitchableDeclarations(
|
|
|
|
|
outputText: MagicString, sourceFile: ts.SourceFile,
|
|
|
|
|
declarations: SwitchableVariableDeclaration[]): void {
|
|
|
|
|
declarations.forEach(declaration => {
|
|
|
|
|
const start = declaration.initializer.getStart();
|
|
|
|
|
const end = declaration.initializer.getEnd();
|
|
|
|
|
const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER);
|
|
|
|
|
outputText.overwrite(start, end, replacement);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add the type parameters to the appropriate functions that return `ModuleWithProviders`
|
|
|
|
|
* structures.
|
|
|
|
|
*
|
|
|
|
|
* This function will only get called on typings files.
|
|
|
|
|
*/
|
|
|
|
|
addModuleWithProvidersParams(
|
|
|
|
|
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
|
|
|
|
|
importManager: ImportManager): void {
|
|
|
|
|
moduleWithProviders.forEach(info => {
|
|
|
|
|
const ngModuleName = info.ngModule.node.name.text;
|
2019-06-06 20:22:32 +01:00
|
|
|
const declarationFile = absoluteFromSourceFile(info.declaration.getSourceFile());
|
|
|
|
|
const ngModuleFile = absoluteFromSourceFile(info.ngModule.node.getSourceFile());
|
2019-04-28 20:48:35 +01:00
|
|
|
const importPath = info.ngModule.viaModule ||
|
|
|
|
|
(declarationFile !== ngModuleFile ?
|
2019-06-06 20:22:32 +01:00
|
|
|
stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) :
|
2019-04-28 20:48:35 +01:00
|
|
|
null);
|
|
|
|
|
const ngModule = generateImportString(importManager, importPath, ngModuleName);
|
|
|
|
|
|
|
|
|
|
if (info.declaration.type) {
|
|
|
|
|
const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ?
|
|
|
|
|
info.declaration.type.typeName :
|
|
|
|
|
null;
|
|
|
|
|
if (this.isCoreModuleWithProvidersType(typeName)) {
|
|
|
|
|
// The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type
|
|
|
|
|
// parameter adding.
|
|
|
|
|
outputText.overwrite(
|
|
|
|
|
info.declaration.type.getStart(), info.declaration.type.getEnd(),
|
|
|
|
|
`ModuleWithProviders<${ngModule}>`);
|
|
|
|
|
} else {
|
|
|
|
|
// The declaration returns an unknown type so we need to convert it to a union that
|
|
|
|
|
// includes the ngModule property.
|
|
|
|
|
const originalTypeString = info.declaration.type.getText();
|
|
|
|
|
outputText.overwrite(
|
|
|
|
|
info.declaration.type.getStart(), info.declaration.type.getEnd(),
|
|
|
|
|
`(${originalTypeString})&{ngModule:${ngModule}}`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// The declaration has no return type so provide one.
|
|
|
|
|
const lastToken = info.declaration.getLastToken();
|
|
|
|
|
const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ?
|
|
|
|
|
lastToken.getStart() :
|
|
|
|
|
info.declaration.getEnd();
|
|
|
|
|
outputText.appendLeft(
|
|
|
|
|
insertPoint,
|
|
|
|
|
`: ${generateImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected findEndOfImports(sf: ts.SourceFile): number {
|
|
|
|
|
for (const stmt of sf.statements) {
|
|
|
|
|
if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) &&
|
|
|
|
|
!ts.isNamespaceImport(stmt)) {
|
|
|
|
|
return stmt.getStart();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
|
|
|
|
|
* @param typeName The type to check.
|
|
|
|
|
* @returns true if the type is the core Angular `ModuleWithProviders` interface.
|
|
|
|
|
*/
|
|
|
|
|
private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) {
|
|
|
|
|
const id =
|
|
|
|
|
typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null;
|
|
|
|
|
return (
|
|
|
|
|
id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findStatement(node: ts.Node) {
|
|
|
|
|
while (node) {
|
|
|
|
|
if (ts.isExpressionStatement(node)) {
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
node = node.parent;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateImportString(
|
|
|
|
|
importManager: ImportManager, importPath: string | null, importName: string) {
|
|
|
|
|
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
|
|
|
|
|
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
|
|
|
|
|
}
|