fix(ivy): properly rewrite imports in generated factory shims (#27998)
Generated factory shims can import from @angular/core. However, we have special logic in place to rewrite self-imports when generating code for @angular/core. This commit leverages the new standalone ImportRewriter interface to properly rewrite imports in generated factory shims. Before this fix, a generated factory file for core would look like: ```typescript import * as i0 from './r3_symbols'; export var ApplicationModuleNgFactory = new ɵNgModuleFactory(...); ``` This is invalid, as ɵNgModuleFactory is just NgModuleFactory when imported via r3_symbols. FW-881 #resolve PR Close #27998
This commit is contained in:
parent
3cf1b62722
commit
6003145422
|
@ -239,7 +239,7 @@ export class NgtscProgram implements api.Program {
|
|||
|
||||
if (this.factoryToSourceInfo !== null) {
|
||||
beforeTransforms.push(
|
||||
generatedFactoryTransform(this.factoryToSourceInfo, this.coreImportsFrom));
|
||||
generatedFactoryTransform(this.factoryToSourceInfo, this.importRewriter));
|
||||
}
|
||||
if (this.isCore) {
|
||||
beforeTransforms.push(ivySwitchTransform);
|
||||
|
|
|
@ -11,6 +11,7 @@ ts_library(
|
|||
module_name = "@angular/compiler-cli/src/ngtsc/shims",
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@ngdeps//@types/node",
|
||||
"@ngdeps//typescript",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {relativePathBetween} from '../../util/src/path';
|
||||
import {ImportRewriter} from '../../imports';
|
||||
import {isNonDeclarationTsPath} from '../../util/src/typescript';
|
||||
|
||||
import {ShimGenerator} from './host';
|
||||
|
@ -106,17 +106,17 @@ export interface FactoryInfo {
|
|||
|
||||
export function generatedFactoryTransform(
|
||||
factoryMap: Map<string, FactoryInfo>,
|
||||
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
|
||||
importRewriter: ImportRewriter): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
|
||||
return (file: ts.SourceFile): ts.SourceFile => {
|
||||
return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file);
|
||||
return transformFactorySourceFile(factoryMap, context, importRewriter, file);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function transformFactorySourceFile(
|
||||
factoryMap: Map<string, FactoryInfo>, context: ts.TransformationContext,
|
||||
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
|
||||
importRewriter: ImportRewriter, file: ts.SourceFile): ts.SourceFile {
|
||||
// If this is not a generated file, it won't have factory info associated with it.
|
||||
if (!factoryMap.has(file.fileName)) {
|
||||
// Don't transform non-generated code.
|
||||
|
@ -125,7 +125,7 @@ function transformFactorySourceFile(
|
|||
|
||||
const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName) !;
|
||||
|
||||
const clone = ts.getMutableClone(file);
|
||||
file = ts.getMutableClone(file);
|
||||
|
||||
// Not every exported factory statement is valid. They were generated before the program was
|
||||
// analyzed, and before ngtsc knew which symbols were actually NgModules. factoryMap contains
|
||||
|
@ -146,17 +146,30 @@ function transformFactorySourceFile(
|
|||
// The statement identified as the ɵNonEmptyModule export.
|
||||
let nonEmptyExport: ts.Statement|null = null;
|
||||
|
||||
// Extracted identifiers which refer to import statements from @angular/core.
|
||||
const coreImportIdentifiers = new Set<string>();
|
||||
|
||||
// Consider all the statements.
|
||||
for (const stmt of file.statements) {
|
||||
// Look for imports to @angular/core.
|
||||
if (coreImportsFrom !== null && ts.isImportDeclaration(stmt) &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@angular/core') {
|
||||
// Update the import path to point to the correct file (coreImportsFrom).
|
||||
const path = relativePathBetween(sourceFilePath, coreImportsFrom.fileName);
|
||||
if (path !== null) {
|
||||
if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier) &&
|
||||
stmt.moduleSpecifier.text === '@angular/core') {
|
||||
// Update the import path to point to the correct file using the ImportRewriter.
|
||||
const rewrittenModuleSpecifier =
|
||||
importRewriter.rewriteSpecifier('@angular/core', sourceFilePath);
|
||||
if (rewrittenModuleSpecifier !== stmt.moduleSpecifier.text) {
|
||||
transformedStatements.push(ts.updateImportDeclaration(
|
||||
stmt, stmt.decorators, stmt.modifiers, stmt.importClause,
|
||||
ts.createStringLiteral(path)));
|
||||
ts.createStringLiteral(rewrittenModuleSpecifier)));
|
||||
|
||||
// Record the identifier by which this imported module goes, so references to its symbols
|
||||
// can be discovered later.
|
||||
if (stmt.importClause !== undefined && stmt.importClause.namedBindings !== undefined &&
|
||||
ts.isNamespaceImport(stmt.importClause.namedBindings)) {
|
||||
coreImportIdentifiers.add(stmt.importClause.namedBindings.name.text);
|
||||
}
|
||||
} else {
|
||||
transformedStatements.push(stmt);
|
||||
}
|
||||
} else if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) {
|
||||
const decl = stmt.declarationList.declarations[0];
|
||||
|
@ -189,6 +202,31 @@ function transformFactorySourceFile(
|
|||
// satisfy closure compiler.
|
||||
transformedStatements.push(nonEmptyExport);
|
||||
}
|
||||
clone.statements = ts.createNodeArray(transformedStatements);
|
||||
return clone;
|
||||
file.statements = ts.createNodeArray(transformedStatements);
|
||||
|
||||
// If any imports to @angular/core were detected and rewritten (which happens when compiling
|
||||
// @angular/core), go through the SourceFile and rewrite references to symbols imported from core.
|
||||
if (coreImportIdentifiers.size > 0) {
|
||||
const visit = <T extends ts.Node>(node: T): T => {
|
||||
node = ts.visitEachChild(node, child => visit(child), context);
|
||||
|
||||
// Look for expressions of the form "i.s" where 'i' is a detected name for an @angular/core
|
||||
// import that was changed above. Rewrite 's' using the ImportResolver.
|
||||
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) &&
|
||||
coreImportIdentifiers.has(node.expression.text)) {
|
||||
// This is an import of a symbol from @angular/core. Transform it with the importRewriter.
|
||||
const rewrittenSymbol = importRewriter.rewriteSymbol(node.name.text, '@angular/core');
|
||||
if (rewrittenSymbol !== node.name.text) {
|
||||
const updated =
|
||||
ts.updatePropertyAccess(node, node.expression, ts.createIdentifier(rewrittenSymbol));
|
||||
node = updated as T & ts.PropertyAccessExpression;
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
file = visit(file);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
|
|
@ -905,6 +905,29 @@ describe('ngtsc behavioral tests', () => {
|
|||
expect(emptyFactory).toContain(`export var ɵNonEmptyModule = true;`);
|
||||
});
|
||||
|
||||
it('should generate correct imports in factory stubs when compiling @angular/core', () => {
|
||||
env.tsconfig({'allowEmptyCodegenFiles': true});
|
||||
|
||||
env.write('test.ts', `
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
@NgModule({})
|
||||
export class TestModule {}
|
||||
`);
|
||||
|
||||
// Trick the compiler into thinking it's compiling @angular/core.
|
||||
env.write('r3_symbols.ts', 'export const ITS_JUST_ANGULAR = true;');
|
||||
|
||||
env.driveMain();
|
||||
|
||||
const factoryContents = env.getContents('test.ngfactory.js');
|
||||
expect(normalize(factoryContents)).toBe(normalize(`
|
||||
import * as i0 from "./r3_symbols";
|
||||
import { TestModule } from './test';
|
||||
export var TestModuleNgFactory = new i0.NgModuleFactory(TestModule);
|
||||
`));
|
||||
});
|
||||
|
||||
it('should generate a summary stub for decorated classes in the input file only', () => {
|
||||
env.tsconfig({'allowEmptyCodegenFiles': true});
|
||||
|
||||
|
@ -1635,3 +1658,7 @@ function expectTokenAtPosition<T extends ts.Node>(
|
|||
expect(guard(node)).toBe(true);
|
||||
return node as T;
|
||||
}
|
||||
|
||||
function normalize(input: string): string {
|
||||
return input.replace(/\s+/g, ' ').trim();
|
||||
}
|
Loading…
Reference in New Issue