fix(ivy): ngcc - separate typings rendering from src rendering (#25445)
Previously the same `Renderer` was used to render typings (.d.ts) files. But the new `UmdRenderer` is not able to render typings files correctly. This commit splits out the typings rendering from the src rendering. To achieve this the previous renderers have been refactored from sub-classes of the abstract `Renderer` class to classes that implement the `RenderingFormatter` interface, which are then passed to the `Renderer` and `DtsRenderer` to modify its rendering behaviour. Along the way a few utility interfaces and classes have been moved around and renamed for clarity. PR Close #25445
This commit is contained in:

committed by
Jason Aden

parent
f4655ea98a
commit
c613596658
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @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';
|
||||
import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||
import {Import, ImportManager} from '../../../src/ngtsc/translator';
|
||||
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
|
||||
import {CompiledClass} from '../analysis/decoration_analyzer';
|
||||
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);
|
||||
const relativePath =
|
||||
'./' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath);
|
||||
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}`);
|
||||
}
|
||||
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
|
||||
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;
|
||||
const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile());
|
||||
const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile());
|
||||
const importPath = info.ngModule.viaModule ||
|
||||
(declarationFile !== ngModuleFile ?
|
||||
stripExtension(
|
||||
`./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) :
|
||||
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}`;
|
||||
}
|
Reference in New Issue
Block a user