diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 157e6d111d..177b7df1e6 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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); diff --git a/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel b/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel index d936b34d9b..1d29e79f50 100644 --- a/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/shims/BUILD.bazel @@ -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", diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts index 7fce35e274..d59656a443 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts @@ -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, - coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory { + importRewriter: ImportRewriter): ts.TransformerFactory { return (context: ts.TransformationContext): ts.Transformer => { return (file: ts.SourceFile): ts.SourceFile => { - return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file); + return transformFactorySourceFile(factoryMap, context, importRewriter, file); }; }; } function transformFactorySourceFile( factoryMap: Map, 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(); + // 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 = (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; } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 19b34ac4a7..66f2bdd6b4 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -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( expect(guard(node)).toBe(true); return node as T; } + +function normalize(input: string): string { + return input.replace(/\s+/g, ' ').trim(); +} \ No newline at end of file