From 423b39e216666adb15d3018aa652623f32377b15 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 1 Feb 2019 17:24:21 -0800 Subject: [PATCH] feat(ivy): use fileNameToModuleName to emit imports when it's available (#28523) The ultimate goal of this commit is to make use of fileNameToModuleName to get the module specifier to use when generating an import, when that API is available in the CompilerHost that ngtsc is created with. As part of getting there, the way in which ngtsc tracks references and generates import module specifiers is refactored considerably. References are tracked with the Reference class, and previously ngtsc had several different kinds of Reference. An AbsoluteReference represented a declaration which needed to be imported via an absolute module specifier tracked in the AbsoluteReference, and a RelativeReference represented a declaration from the local program, imported via relative path or referred to directly by identifier if possible. Thus, how to refer to a particular declaration was encoded into the Reference type _at the time of creation of the Reference_. This commit refactors that logic and reduces Reference to a single class with no subclasses. A Reference represents a node being referenced, plus context about how the node was located. This context includes a "bestGuessOwningModule", the compiler's best guess at which absolute module specifier has defined this reference. For example, if the compiler arrives at the declaration of CommonModule via an import to @angular/common, then any references obtained from CommonModule (e.g. NgIf) will also be considered to be owned by @angular/common. A ReferenceEmitter class and accompanying ReferenceEmitStrategy interface are introduced. To produce an Expression referring to a given Reference'd node, the ReferenceEmitter consults a sequence of ReferenceEmitStrategy implementations. Several different strategies are defined: - LocalIdentifierStrategy: use local ts.Identifiers if available. - AbsoluteModuleStrategy: if the Reference has a bestGuessOwningModule, import the node via an absolute import from that module specifier. - LogicalProjectStrategy: if the Reference is in the logical project (is under the project rootDirs), import the node via a relative import. - FileToModuleStrategy: use a FileToModuleHost to generate the module specifier by which to import the node. Depending on the availability of fileNameToModuleName in the CompilerHost, then, a different collection of these strategies is used for compilation. PR Close #28523 --- packages/bazel/src/ng_module.bzl | 1 + packages/compiler-cli/BUILD.bazel | 1 + packages/compiler-cli/src/ngcc/BUILD.bazel | 1 + .../ngcc/src/analysis/decoration_analyzer.ts | 20 +- .../module_with_providers_analyzer.ts | 7 +- .../src/analysis/ngcc_references_registry.ts | 6 +- .../ngcc/src/packages/entry_point_bundle.ts | 7 +- .../compiler-cli/src/ngcc/test/BUILD.bazel | 1 + .../test/analysis/decoration_analyzer_spec.ts | 7 +- .../private_declarations_analyzer_spec.ts | 14 +- .../test/analysis/references_registry_spec.ts | 8 +- .../src/ngcc/test/helpers/utils.ts | 4 +- .../test/rendering/esm2015_renderer_spec.ts | 10 +- .../ngcc/test/rendering/esm5_renderer_spec.ts | 10 +- .../src/ngtsc/annotations/src/component.ts | 4 +- .../src/ngtsc/annotations/src/directive.ts | 4 +- .../src/ngtsc/annotations/src/ng_module.ts | 30 +- .../ngtsc/annotations/src/selector_scope.ts | 42 ++- .../src/ngtsc/annotations/src/util.ts | 14 +- .../src/ngtsc/annotations/test/BUILD.bazel | 2 + .../ngtsc/annotations/test/component_spec.ts | 10 +- .../annotations/test/selector_scope_spec.ts | 45 ++- .../src/ngtsc/imports/BUILD.bazel | 1 + .../compiler-cli/src/ngtsc/imports/index.ts | 5 +- .../src/ngtsc/imports/src/emitter.ts | 271 ++++++++++++++++++ .../src/ngtsc/imports/src/find_export.ts | 47 +++ .../src/ngtsc/imports/src/references.ts | 193 +++++-------- .../src/ngtsc/imports/src/resolver.ts | 102 +------ .../ngtsc/partial_evaluator/src/interface.ts | 8 +- .../partial_evaluator/src/interpreter.ts | 38 ++- .../partial_evaluator/test/evaluator_spec.ts | 44 ++- packages/compiler-cli/src/ngtsc/program.ts | 55 ++-- .../src/ngtsc/routing/src/lazy.ts | 14 +- .../src/ngtsc/typecheck/src/context.ts | 17 +- .../ngtsc/typecheck/src/type_check_block.ts | 13 +- .../src/ngtsc/typecheck/test/BUILD.bazel | 3 + .../typecheck/test/type_constructor_spec.ts | 14 +- .../compiler-cli/src/ngtsc/util/BUILD.bazel | 1 + .../src/ngtsc/util/src/typescript.ts | 46 +++ packages/compiler-cli/src/transformers/api.ts | 6 + 40 files changed, 709 insertions(+), 417 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/imports/src/emitter.ts create mode 100644 packages/compiler-cli/src/ngtsc/imports/src/find_export.ts diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 54abf74c97..e6289e8640 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -254,6 +254,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs): "createExternalSymbolFactoryReexports": (not _is_bazel()), # FIXME: wrong place to de-dupe "expectedOut": depset([o.path for o in expected_outs]).to_list(), + "_useHostForImportGeneration": (not _is_bazel()), } if _should_produce_flat_module_outs(ctx): diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 4e4688a6f4..dcd274b1e1 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -29,6 +29,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/entry_point", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/shims", diff --git a/packages/compiler-cli/src/ngcc/BUILD.bazel b/packages/compiler-cli/src/ngcc/BUILD.bazel index bcc5f8f01f..1915c0e0fd 100644 --- a/packages/compiler-cli/src/ngcc/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/translator", diff --git a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts index 2224c3723a..a5dc35592b 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts @@ -12,8 +12,9 @@ import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations'; import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles'; -import {ModuleResolver, TsReferenceResolver} from '../../../ngtsc/imports'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../../ngtsc/imports'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; +import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform'; import {DecoratedClass} from '../host/decorated_class'; import {NgccReflectionHost} from '../host/ngcc_host'; @@ -62,9 +63,16 @@ class NgccResourceLoader implements ResourceLoader { */ export class DecorationAnalyzer { resourceManager = new NgccResourceLoader(); - resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host); - scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver); - evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver); + refEmitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host), + // TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc + // projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based + // on whether a bestGuessOwningModule is present in the Reference. + new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)), + ]); + scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.refEmitter); + evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker); moduleResolver = new ModuleResolver(this.program, this.options, this.host); importGraph = new ImportGraph(this.moduleResolver); cycleAnalyzer = new CycleAnalyzer(this.importGraph); @@ -79,7 +87,7 @@ export class DecorationAnalyzer { new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false), new NgModuleDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry, - this.isCore, /* routeAnalyzer */ null), + this.isCore, /* routeAnalyzer */ null, this.refEmitter), new PipeDecoratorHandler(this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore), ]; @@ -87,7 +95,7 @@ export class DecorationAnalyzer { private program: ts.Program, private options: ts.CompilerOptions, private host: ts.CompilerHost, private typeChecker: ts.TypeChecker, private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry, - private rootDirs: string[], private isCore: boolean) {} + private rootDirs: AbsoluteFsPath[], private isCore: boolean) {} /** * Analyze a program to find all the decorated files should be transformed. diff --git a/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts index 5456f19543..6155c19890 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import {ReferencesRegistry} from '../../../ngtsc/annotations'; -import {ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {Declaration} from '../../../ngtsc/reflection'; import {NgccReflectionHost} from '../host/ngcc_host'; import {isDefined} from '../utils'; @@ -62,8 +62,9 @@ export class ModuleWithProvidersAnalyzer { `The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`); } // Record the usage of the internal module as it needs to become an exported symbol - this.referencesRegistry.add( - ngModule.node, new ResolvedReference(ngModule.node, fn.ngModule)); + const reference = new Reference(ngModule.node); + reference.addIdentifier(fn.ngModule); + this.referencesRegistry.add(ngModule.node, reference); ngModule = {node: dtsNgModule, viaModule: null}; } diff --git a/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts b/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts index c0000c1ac2..08a7f86e24 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/ngcc_references_registry.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import {ReferencesRegistry} from '../../../ngtsc/annotations'; -import {Reference, ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {Declaration, ReflectionHost} from '../../../ngtsc/reflection'; import {hasNameIdentifier} from '../utils'; @@ -31,8 +31,8 @@ export class NgccReferencesRegistry implements ReferencesRegistry { */ add(source: ts.Declaration, ...references: Reference[]): void { references.forEach(ref => { - // Only store resolved references. We are not interested in literals. - if (ref instanceof ResolvedReference && hasNameIdentifier(ref.node)) { + // Only store relative references. We are not interested in literals. + if (ref.bestGuessOwningModule === null && hasNameIdentifier(ref.node)) { const declaration = this.host.getDeclarationOfIdentifier(ref.node.name); if (declaration && hasNameIdentifier(declaration.node)) { this.map.set(declaration.node.name, declaration); diff --git a/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts index 3dbbaee262..d04da8fe7e 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/entry_point_bundle.ts @@ -7,10 +7,13 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; + import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPoint, EntryPointFormat} from './entry_point'; + /** * A bundle of files and paths (and TS programs) that correspond to a particular * format of a package entry-point. @@ -18,7 +21,7 @@ import {EntryPoint, EntryPointFormat} from './entry_point'; export interface EntryPointBundle { format: EntryPointFormat; isFlat: boolean; - rootDirs: string[]; + rootDirs: AbsoluteFsPath[]; src: BundleProgram; dts: BundleProgram|null; } @@ -45,7 +48,7 @@ export function makeEntryPointBundle( rootDir: entryPoint.path, }; const host = ts.createCompilerHost(options); - const rootDirs = [entryPoint.path]; + const rootDirs = [AbsoluteFsPath.from(entryPoint.path)]; // Create the bundle programs, as necessary. const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host); diff --git a/packages/compiler-cli/src/ngcc/test/BUILD.bazel b/packages/compiler-cli/src/ngcc/test/BUILD.bazel index 72ccc7d222..f1b6941b95 100644 --- a/packages/compiler-cli/src/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/test/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//packages/compiler-cli/src/ngcc", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/transform", diff --git a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts index 889b8dbbec..5798fae8b9 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {Decorator} from '../../../ngtsc/reflection'; import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform'; import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; @@ -99,7 +100,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); testHandler = createTestHandler(); analyzer.handlers = [testHandler]; result = analyzer.analyzeProgram(); @@ -143,7 +144,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); const testHandler = createTestHandler(); analyzer.handlers = [testHandler]; const result = analyzer.analyzeProgram(); @@ -161,7 +162,7 @@ describe('DecorationAnalyzer', () => { const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const analyzer = new DecorationAnalyzer( program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry, - [''], false); + [AbsoluteFsPath.fromUnchecked('/')], false); const testHandler = createTestHandler(); analyzer.handlers = [testHandler]; const result = analyzer.analyzeProgram(); diff --git a/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts index 4adbf8bbaa..ed74e29212 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/private_declarations_analyzer_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {ResolvedReference} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; @@ -136,19 +136,13 @@ describe('PrivateDeclarationsAnalyzer', () => { // decoration handlers in the `DecorationAnalyzer`. const publicComponentDeclaration = getDeclaration(program, '/src/a.js', 'PublicComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, - new ResolvedReference(publicComponentDeclaration, publicComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(publicComponentDeclaration)); const privateComponentDeclaration = getDeclaration(program, '/src/b.js', 'PrivateComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, new ResolvedReference( - privateComponentDeclaration, privateComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(privateComponentDeclaration)); const internalComponentDeclaration = getDeclaration(program, '/src/c.js', 'InternalComponent', ts.isClassDeclaration); - referencesRegistry.add( - null !, new ResolvedReference( - internalComponentDeclaration, internalComponentDeclaration.name !)); + referencesRegistry.add(null !, new Reference(internalComponentDeclaration)); const analyses = analyzer.analyzeProgram(program); expect(analyses.length).toEqual(2); diff --git a/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts index 230ed7f8c5..5f75dae924 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/references_registry_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Reference, TsReferenceResolver} from '../../../ngtsc/imports'; +import {Reference} from '../../../ngtsc/imports'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; import {TypeScriptReflectionHost} from '../../../ngtsc/reflection'; import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; @@ -39,11 +39,11 @@ describe('NgccReferencesRegistry', () => { const testArrayExpression = testArrayDeclaration.initializer !; const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const registry = new NgccReferencesRegistry(reflectionHost); - const references = evaluator.evaluate(testArrayExpression) as Reference[]; + const references = (evaluator.evaluate(testArrayExpression) as any[]) + .filter(ref => ref instanceof Reference) as Reference[]; registry.add(null !, ...references); const map = registry.getDeclarationMap(); diff --git a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts index 68e3f5c9b0..917ac21422 100644 --- a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript'; import {BundleProgram} from '../../src/packages/bundle_program'; import {EntryPointFormat} from '../../src/packages/entry_point'; @@ -15,6 +16,7 @@ import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript'; + /** * * @param format The format of the bundle. @@ -27,7 +29,7 @@ export function makeTestEntryPointBundle( const src = makeTestBundleProgram(files); const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null; const isFlat = src.r3SymbolsFile === null; - return {format, rootDirs: ['/'], src, dts, isFlat}; + return {format, rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isFlat}; } /** diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index bce94403d7..a45ad7e28f 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -8,6 +8,7 @@ import {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; @@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) { const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm2015ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, - typeChecker, host, referencesRegistry, [''], false) - .analyzeProgram(); + const decorationAnalyses = + new DecorationAnalyzer( + bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new EsmRenderer(host, false, bundle, dir, dir); return { diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts index ccc4299f3d..fe8bf2aaa3 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts @@ -8,6 +8,7 @@ import {dirname} from 'canonical-path'; import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../../ngtsc/path'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; @@ -21,10 +22,11 @@ function setup(file: {name: string, contents: string}) { const typeChecker = bundle.src.program.getTypeChecker(); const host = new Esm5ReflectionHost(false, typeChecker); const referencesRegistry = new NgccReferencesRegistry(host); - const decorationAnalyses = new DecorationAnalyzer( - bundle.src.program, bundle.src.options, bundle.src.host, - typeChecker, host, referencesRegistry, [''], false) - .analyzeProgram(); + const decorationAnalyses = + new DecorationAnalyzer( + bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5Renderer(host, false, bundle, dir, dir); return { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 019c87a685..20650763d4 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -12,7 +12,7 @@ import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ModuleResolver, ResolvedReference} from '../../imports'; +import {ModuleResolver, Reference} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; @@ -225,7 +225,7 @@ export class ComponentDecoratorHandler implements // If the component has a selector, it should be registered with the `SelectorScopeRegistry` so // when this component appears in an `@NgModule` scope, its selector can be determined. if (metadata.selector !== null) { - const ref = new ResolvedReference(node, node.name !); + const ref = new Reference(node); this.scopeRegistry.registerDirective(node, { ref, name: node.name !.text, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 6a7ddfc0c2..f68244a849 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -10,7 +10,7 @@ import {ConstantPool, Expression, ParseError, R3DirectiveMetadata, R3QueryMetada import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {Reference, ResolvedReference} from '../../imports'; +import {Reference} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; @@ -57,7 +57,7 @@ export class DirectiveDecoratorHandler implements // If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so // when this directive appears in an `@NgModule` scope, its selector can be determined. if (analysis && analysis.selector !== null) { - let ref = new ResolvedReference(node, node.name !); + const ref = new Reference(node); this.scopeRegistry.registerDirective(node, { ref, directive: ref, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 9b5cc2d361..33ae5ee7f3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -10,11 +10,12 @@ import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identi import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {Reference, ResolvedReference} from '../../imports'; +import {Reference, ReferenceEmitter} from '../../imports'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; +import {getSourceFile} from '../../util/src/typescript'; import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; @@ -37,7 +38,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { directives.push(directive.ref.toExpression(context) !); }); - scope.pipes.forEach(pipe => pipes.push(pipe.toExpression(context) !)); + (directive, _) => { directives.push(this.refEmitter.emit(directive.ref, context) !); }); + scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !)); const directiveArray = new LiteralArrayExpr(directives); const pipesArray = new LiteralArrayExpr(pipes); - const declExpr = decl.toExpression(context) !; + const declExpr = this.refEmitter.emit(decl, context) !; const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope); const callExpr = new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]); @@ -221,15 +220,15 @@ export class NgModuleDecoratorHandler implements DecoratorHandler, valueContext: ts.SourceFile, typeContext: ts.SourceFile): R3Reference { - if (!(valueRef instanceof ResolvedReference)) { - return toR3Reference(valueRef, valueRef, valueContext, valueContext); + if (valueRef.hasOwningModuleGuess) { + return toR3Reference(valueRef, valueRef, valueContext, valueContext, this.refEmitter); } else { let typeRef = valueRef; let typeNode = this.reflector.getDtsDeclaration(typeRef.node); if (typeNode !== null && ts.isClassDeclaration(typeNode)) { - typeRef = new ResolvedReference(typeNode, typeNode.name !); + typeRef = new Reference(typeNode); } - return toR3Reference(valueRef, typeRef, valueContext, typeContext); + return toR3Reference(valueRef, typeRef, valueContext, typeContext, this.refEmitter); } } @@ -328,10 +327,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler|null { const scope = this.lookupCompilationScopeAsRefs(node); - return scope !== null ? convertScopeToExpressions(scope, node) : null; + return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null; } private lookupScopesOrDie( @@ -273,20 +273,20 @@ export class SelectorScopeRegistry { // Expand imports to the exported scope of those imports. ...flatten(data.imports.map( ref => - this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context) + this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context) .exported)), // And include the compilation scope of exported modules. ...flatten( data.exports .map( ref => this.lookupScopes( - ref.node as ts.Declaration, absoluteModuleName(ref), context)) + ref.node as ts.Declaration, ref.ownedByModuleGuess, context)) .filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null) .map(scope => scope.exported)) ], exported: flatten(data.exports.map(ref => { const scope = - this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref), context); + this.lookupScopes(ref.node as ts.Declaration, ref.ownedByModuleGuess, context); if (scope !== null) { return scope.exported; } else { @@ -438,11 +438,11 @@ export class SelectorScopeRegistry { const type = element.exprName; if (ngModuleImportedFrom !== null) { const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); - const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); - return this.resolver.resolve(node, moduleName, resolutionContext); + const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); + return new Reference(node, {specifier, resolutionContext}); } else { const {node} = reflectTypeEntityToDeclaration(type, this.checker); - return this.resolver.resolve(node, null, resolutionContext); + return new Reference(node); } }); } @@ -455,17 +455,11 @@ function flatten(array: T[][]): T[] { }, [] as T[]); } -function absoluteModuleName(ref: Reference): string|null { - if (!(ref instanceof AbsoluteReference)) { - return null; - } - return ref.moduleName; -} - function convertDirectiveReferenceList( - input: ScopeDirective[], context: ts.SourceFile): ScopeDirective[] { + input: ScopeDirective[], context: ts.SourceFile, + refEmitter: ReferenceEmitter): ScopeDirective[] { return input.map(meta => { - const directive = meta.directive.toExpression(context); + const directive = refEmitter.emit(meta.directive, context); if (directive === null) { throw new Error(`Could not write expression to reference ${meta.directive.node}`); } @@ -474,10 +468,11 @@ function convertDirectiveReferenceList( } function convertPipeReferenceMap( - map: Map, context: ts.SourceFile): Map { + map: Map, context: ts.SourceFile, + refEmitter: ReferenceEmitter): Map { const newMap = new Map(); map.forEach((meta, selector) => { - const pipe = meta.toExpression(context); + const pipe = refEmitter.emit(meta, context); if (pipe === null) { throw new Error(`Could not write expression to reference ${meta.node}`); } @@ -487,10 +482,11 @@ function convertPipeReferenceMap( } function convertScopeToExpressions( - scope: CompilationScope, context: ts.Declaration): CompilationScope { + scope: CompilationScope, context: ts.Declaration, + refEmitter: ReferenceEmitter): CompilationScope { const sourceContext = ts.getOriginalNode(context).getSourceFile(); - const directives = convertDirectiveReferenceList(scope.directives, sourceContext); - const pipes = convertPipeReferenceMap(scope.pipes, sourceContext); + const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter); + const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter); const declPointer = maybeUnwrapNameOfDeclaration(context); let containsForwardDecls = false; directives.forEach(meta => { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 947b6d332b..1bd49291ce 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -10,7 +10,7 @@ import {R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNode import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {AbsoluteReference, ImportMode, Reference} from '../../imports'; +import {ImportMode, Reference, ReferenceEmitter} from '../../imports'; import {ClassMemberKind, CtorParameter, Decorator, ReflectionHost} from '../../reflection'; export enum ConstructorDepErrorKind { @@ -119,9 +119,9 @@ export function validateConstructorDependencies( export function toR3Reference( valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile, - typeContext: ts.SourceFile): R3Reference { - const value = valueRef.toExpression(valueContext, ImportMode.UseExistingImport); - const type = typeRef.toExpression(typeContext, ImportMode.ForceNewImport); + typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference { + const value = refEmitter.emit(valueRef, valueContext, ImportMode.UseExistingImport); + const type = refEmitter.emit(typeRef, typeContext, ImportMode.ForceNewImport); if (value === null || type === null) { throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`); } @@ -133,8 +133,7 @@ export function isAngularCore(decorator: Decorator): boolean { } export function isAngularCoreReference(reference: Reference, symbolName: string) { - return reference instanceof AbsoluteReference && reference.moduleName === '@angular/core' && - reference.symbolName === symbolName; + return reference.ownedByModuleGuess === '@angular/core' && reference.debugName === symbolName; } /** @@ -209,8 +208,7 @@ export function unwrapForwardRef(node: ts.Expression, reflector: ReflectionHost) export function forwardRefResolver( ref: Reference, args: ts.Expression[]): ts.Expression|null { - if (!(ref instanceof AbsoluteReference) || ref.moduleName !== '@angular/core' || - ref.symbolName !== 'forwardRef' || args.length !== 1) { + if (!isAngularCoreReference(ref, 'forwardRef') || args.length !== 1) { return null; } return expandForwardRef(args[0]); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index 0379b2da14..b179d74b6b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -16,9 +16,11 @@ ts_library( "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/translator", + "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index 08abb0b890..f8a65e2a4b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ModuleResolver, TsReferenceResolver} from '../../imports'; +import {ModuleResolver, ReferenceEmitter} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; @@ -44,15 +44,15 @@ describe('ComponentDecoratorHandler', () => { ]); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const moduleResolver = new ModuleResolver(program, options, host); const importGraph = new ImportGraph(moduleResolver); const cycleAnalyzer = new CycleAnalyzer(importGraph); const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver), - false, new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer); + reflectionHost, evaluator, + new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false, + new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer); const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); if (detected === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts index 56785a28c8..92f231950f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts @@ -8,9 +8,11 @@ import * as ts from 'typescript'; -import {AbsoluteReference, ResolvedReference, TsReferenceResolver} from '../../imports'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports'; +import {LogicalFileSystem} from '../../path'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getRootDirs} from '../../util/src/typescript'; import {SelectorScopeRegistry} from '../src/selector_scope'; describe('SelectorScopeRegistry', () => { @@ -63,18 +65,19 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); - const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); - - const resolver = new TsReferenceResolver(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver); + const ProgramCmpRef = new Reference(ProgramCmp); + const refEmitter = makeReferenceEmitter(program, checker, options, host); + const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); registry.registerModule(ProgramModule, { - declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], + declarations: [new Reference(ProgramCmp)], exports: [], - imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], + imports: [new Reference( + SomeModule, + {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], }); - const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !); + const ref = new Reference(ProgramCmp); registry.registerDirective(ProgramCmp, { name: 'ProgramCmp', ref: ProgramCmpRef, @@ -136,14 +139,15 @@ describe('SelectorScopeRegistry', () => { expect(ProgramModule).toBeDefined(); expect(SomeModule).toBeDefined(); - const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !); - - const resolver = new TsReferenceResolver(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver); + const ProgramCmpRef = new Reference(ProgramCmp); + const refEmitter = makeReferenceEmitter(program, checker, options, host); + const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); registry.registerModule(ProgramModule, { - declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)], - exports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')], + declarations: [new Reference(ProgramCmp)], + exports: [new Reference( + SomeModule, + {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], imports: [], }); @@ -166,4 +170,15 @@ describe('SelectorScopeRegistry', () => { expect(scope.directives).toBeDefined(); expect(scope.directives.length).toBe(2); }); -}); \ No newline at end of file +}); + +function makeReferenceEmitter( + program: ts.Program, checker: ts.TypeChecker, options: ts.CompilerOptions, + host: ts.CompilerHost): ReferenceEmitter { + const rootDirs = getRootDirs(host, options); + return new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy(program, checker, options, host), + new LogicalProjectStrategy(checker, new LogicalFileSystem(rootDirs)), + ]); +} diff --git a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel index 7765082b2b..6f8cb487a9 100644 --- a/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/imports/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( deps = [ "//packages:types", "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//@types/node", "@ngdeps//typescript", diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 386775da9d..50af9d405d 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -7,5 +7,6 @@ */ export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; -export {AbsoluteReference, ImportMode, NodeReference, Reference, ResolvedReference} from './src/references'; -export {ModuleResolver, ReferenceResolver, TsReferenceResolver} from './src/resolver'; +export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter'; +export {ImportMode, OwningModule, Reference} from './src/references'; +export {ModuleResolver} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts new file mode 100644 index 0000000000..c52d5a4d39 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts @@ -0,0 +1,271 @@ +/** + * @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 {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler'; +import {ExternalReference} from '@angular/compiler/src/compiler'; +import * as ts from 'typescript'; + +import {LogicalFileSystem, LogicalProjectPath} from '../../path'; +import {getSourceFile, isDeclaration, nodeNameForError} from '../../util/src/typescript'; + +import {findExportedNameOfNode} from './find_export'; +import {ImportMode, Reference} from './references'; + +/** + * A host which supports an operation to convert a file name into a module name. + * + * This operation is typically implemented as part of the compiler host passed to ngtsc when running + * under a build tool like Bazel or Blaze. + */ +export interface FileToModuleHost { + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string; +} + +/** + * A particular strategy for generating an expression which refers to a `Reference`. + * + * There are many potential ways a given `Reference` could be referred to in the context of a given + * file. A local declaration could be available, the `Reference` could be importable via a relative + * import within the project, or an absolute import into `node_modules` might be necessary. + * + * Different `ReferenceEmitStrategy` implementations implement specific logic for generating such + * references. A single strategy (such as using a local declaration) may not always be able to + * generate an expression for every `Reference` (for example, if no local identifier is available), + * and may return `null` in such a case. + */ +export interface ReferenceEmitStrategy { + /** + * Emit an `Expression` which refers to the given `Reference` in the context of a particular + * source file, if possible. + * + * @param ref the `Reference` for which to generate an expression + * @param context the source file in which the `Expression` must be valid + * @param importMode a flag which controls whether imports should be generated or not + * @returns an `Expression` which refers to the `Reference`, or `null` if none can be generated + */ + emit(ref: Reference, context: ts.SourceFile, importMode: ImportMode): Expression|null; +} + +/** + * Generates `Expression`s which refer to `Reference`s in a given context. + * + * A `ReferenceEmitter` uses one or more `ReferenceEmitStrategy` implementations to produce a + * an `Expression` which refers to a `Reference` in the context of a particular file. + */ +export class ReferenceEmitter { + constructor(private strategies: ReferenceEmitStrategy[]) {} + + emit( + ref: Reference, context: ts.SourceFile, + importMode: ImportMode = ImportMode.UseExistingImport): Expression { + for (const strategy of this.strategies) { + const emitted = strategy.emit(ref, context, importMode); + if (emitted !== null) { + return emitted; + } + } + throw new Error( + `Unable to write a reference to ${nodeNameForError(ref.node)} in ${ref.node.getSourceFile().fileName} from ${context.fileName}`); + } +} + +/** + * A `ReferenceEmitStrategy` which will refer to declarations by any local `ts.Identifier`s, if + * such identifiers are available. + */ +export class LocalIdentifierStrategy implements ReferenceEmitStrategy { + emit(ref: Reference, context: ts.SourceFile, importMode: ImportMode): Expression|null { + // If the emitter has specified ForceNewImport, then LocalIdentifierStrategy should not use a + // local identifier at all, *except* in the source file where the node is actually declared. + if (importMode === ImportMode.ForceNewImport && + getSourceFile(ref.node) !== getSourceFile(context)) { + return null; + } + + // A Reference can have multiple identities in different files, so it may already have an + // Identifier in the requested context file. + const identifier = ref.getIdentityIn(context); + if (identifier !== null) { + return new WrappedNodeExpr(identifier); + } else { + return null; + } + } +} + +/** + * A `ReferenceEmitStrategy` which will refer to declarations that come from `node_modules` using + * an absolute import. + * + * Part of this strategy involves looking at the target entry point and identifying the exported + * name of the targeted declaration, as it might be different from the declared name (e.g. a + * directive might be declared as FooDirImpl, but exported as FooDir). If no export can be found + * which maps back to the original directive, an error is thrown. + */ +export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { + /** + * A cache of the exports of specific modules, because resolving a module to its exports is a + * costly operation. + */ + private moduleExportsCache = new Map|null>(); + + constructor( + private program: ts.Program, private checker: ts.TypeChecker, + private options: ts.CompilerOptions, private host: ts.CompilerHost) {} + + emit(ref: Reference, context: ts.SourceFile, importMode: ImportMode): Expression|null { + if (ref.bestGuessOwningModule === null) { + // There is no module name available for this Reference, meaning it was arrived at via a + // relative path. + return null; + } else if (!isDeclaration(ref.node)) { + // It's not possible to import something which isn't a declaration. + throw new Error('Debug assert: importing a Reference to non-declaration?'); + } + + // Try to find the exported name of the declaration, if one is available. + const {specifier, resolutionContext} = ref.bestGuessOwningModule; + const symbolName = this.resolveImportName(specifier, ref.node, resolutionContext); + if (symbolName === null) { + // TODO(alxhub): make this error a ts.Diagnostic pointing at whatever caused this import to be + // triggered. + throw new Error( + `Symbol ${ref.debugName} declared in ${getSourceFile(ref.node).fileName} is not exported from ${specifier} (import into ${context.fileName})`); + } + + return new ExternalExpr(new ExternalReference(specifier, symbolName)); + } + + private resolveImportName(moduleName: string, target: ts.Declaration, fromFile: string): string + |null { + const exports = this.getExportsOfModule(moduleName, fromFile); + if (exports !== null && exports.has(target)) { + return exports.get(target) !; + } else { + return null; + } + } + + private getExportsOfModule(moduleName: string, fromFile: string): + Map|null { + if (!this.moduleExportsCache.has(moduleName)) { + this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile)); + } + return this.moduleExportsCache.get(moduleName) !; + } + + private enumerateExportsOfModule(specifier: string, fromFile: string): + Map|null { + // First, resolve the module specifier to its entry point, and get the ts.Symbol for it. + const resolved = ts.resolveModuleName(specifier, fromFile, this.options, this.host); + if (resolved.resolvedModule === undefined) { + return null; + } + + const entryPointFile = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName); + if (entryPointFile === undefined) { + return null; + } + + const entryPointSymbol = this.checker.getSymbolAtLocation(entryPointFile); + if (entryPointSymbol === undefined) { + return null; + } + + // Next, build a Map of all the ts.Declarations exported via the specifier and their exported + // names. + const exportMap = new Map(); + + const exports = this.checker.getExportsOfModule(entryPointSymbol); + for (const expSymbol of exports) { + // Resolve export symbols to their actual declarations. + const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ? + this.checker.getAliasedSymbol(expSymbol) : + expSymbol; + + // At this point the valueDeclaration of the symbol should be defined. + const decl = declSymbol.valueDeclaration; + if (decl === undefined) { + continue; + } + + // Prefer importing the symbol via its declared name, but take any export of it otherwise. + if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) { + exportMap.set(decl, expSymbol.name); + } + } + + return exportMap; + } +} + +/** + * A `ReferenceEmitStrategy` which will refer to declarations via relative paths, provided they're + * both in the logical project "space" of paths. + * + * This is trickier than it sounds, as the two files may be in different root directories in the + * project. Simply calculating a file system relative path between the two is not sufficient. + * Instead, `LogicalProjectPath`s are used. + */ +export class LogicalProjectStrategy implements ReferenceEmitStrategy { + constructor(private checker: ts.TypeChecker, private logicalFs: LogicalFileSystem) {} + + emit(ref: Reference, context: ts.SourceFile): Expression|null { + const destSf = getSourceFile(ref.node); + + // Compute the relative path from the importing file to the file being imported. This is done + // as a logical path computation, because the two files might be in different rootDirs. + const destPath = this.logicalFs.logicalPathOfSf(destSf); + if (destPath === null) { + // The imported file is not within the logical project filesystem. + return null; + } + + const originPath = this.logicalFs.logicalPathOfSf(context); + if (originPath === null) { + throw new Error( + `Debug assert: attempt to import from ${context.fileName} but it's outside the program?`); + } + + // There's no way to emit a relative reference from a file to itself. + if (destPath === originPath) { + return null; + } + + const name = findExportedNameOfNode(ref.node, destSf, this.checker); + if (name === null) { + // The target declaration isn't exported from the file it's declared in. This is an issue! + return null; + } + + // With both files expressed as LogicalProjectPaths, getting the module specifier as a relative + // path is now straightforward. + const moduleName = LogicalProjectPath.relativePathBetween(originPath, destPath); + return new ExternalExpr({moduleName, name}); + } +} + +/** + * A `ReferenceEmitStrategy` which uses a `FileToModuleHost` to generate absolute import references. + */ +export class FileToModuleStrategy implements ReferenceEmitStrategy { + constructor(private checker: ts.TypeChecker, private fileToModuleHost: FileToModuleHost) {} + + emit(ref: Reference, context: ts.SourceFile): Expression|null { + const destSf = getSourceFile(ref.node); + const name = findExportedNameOfNode(ref.node, destSf, this.checker); + if (name === null) { + return null; + } + + const moduleName = + this.fileToModuleHost.fileNameToModuleName(destSf.fileName, context.fileName); + + return new ExternalExpr({moduleName, name}); + } +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts new file mode 100644 index 0000000000..71a4d45d35 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts @@ -0,0 +1,47 @@ +/** + * @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 * as ts from 'typescript'; + +/** + * Find the name, if any, by which a node is exported from a given file. + */ +export function findExportedNameOfNode( + target: ts.Node, file: ts.SourceFile, checker: ts.TypeChecker): string|null { + // First, get the exports of the file. + const symbol = checker.getSymbolAtLocation(file); + if (symbol === undefined) { + return null; + } + const exports = checker.getExportsOfModule(symbol); + + // Look for the export which declares the node. + const found = exports.find(sym => symbolDeclaresNode(sym, target, checker)); + if (found === undefined) { + throw new Error(`failed to find target in ${file.fileName}`); + } + return found !== undefined ? found.name : null; +} + +/** + * Check whether a given `ts.Symbol` represents a declaration of a given node. + * + * This is not quite as trivial as just checking the declarations, as some nodes are + * `ts.ExportSpecifier`s and need to be unwrapped. + */ +function symbolDeclaresNode(sym: ts.Symbol, node: ts.Node, checker: ts.TypeChecker): boolean { + return sym.declarations.some(decl => { + if (ts.isExportSpecifier(decl)) { + const exportedSymbol = checker.getExportSpecifierLocalTargetSymbol(decl); + if (exportedSymbol !== undefined) { + return symbolDeclaresNode(exportedSymbol, node, checker); + } + } + return decl === node; + }); +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/references.ts b/packages/compiler-cli/src/ngtsc/imports/src/references.ts index 2be1cc4a03..348f77fb32 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/references.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/references.ts @@ -6,145 +6,98 @@ * found in the LICENSE file at https://angular.io/license */ -/// - -import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler'; -import * as path from 'path'; import * as ts from 'typescript'; - -const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/; +import {identifierOfNode} from '../../util/src/typescript'; export enum ImportMode { UseExistingImport, ForceNewImport, } +export interface OwningModule { + specifier: string; + resolutionContext: string; +} + /** - * A reference to a `ts.Node`. + * A `ts.Node` plus the context in which it was discovered. * - * For example, if an expression evaluates to a function or class definition, it will be returned - * as a `Reference` (assuming references are allowed in evaluation). + * A `Reference` is a pointer to a `ts.Node` that was extracted from the program somehow. It + * contains not only the node itself, but the information regarding how the node was located. In + * particular, it might track different identifiers by which the node is exposed, as well as + * potentially a module specifier which might expose the node. + * + * The Angular compiler uses `Reference`s instead of `ts.Node`s when tracking classes or generating + * imports. */ -export abstract class Reference { - constructor(readonly node: T) {} - +export class Reference { /** - * Whether an `Expression` can be generated which references the node. - */ - // TODO(issue/24571): remove '!'. - readonly expressable !: boolean; - - /** - * Generate an `Expression` representing this type, in the context of the given SourceFile. + * The compiler's best guess at an absolute module specifier which owns this `Reference`. * - * This could be a local variable reference, if the symbol is imported, or it could be a new - * import if needed. + * This is usually determined by tracking the import statements which led the compiler to a given + * node. If any of these imports are absolute, it's an indication that the node being imported + * might come from that module. + * + * It is not _guaranteed_ that the node in question is exported from its `bestGuessOwningModule` - + * that is mostly a convention that applies in certain package formats. + * + * If `bestGuessOwningModule` is `null`, then it's likely the node came from the current program. */ - abstract toExpression(context: ts.SourceFile, importMode?: ImportMode): Expression|null; + readonly bestGuessOwningModule: OwningModule|null; - abstract addIdentifier(identifier: ts.Identifier): void; -} - -/** - * A reference to a node only, without any ability to get an `Expression` representing that node. - * - * This is used for returning references to things like method declarations, which are not directly - * referenceable. - */ -export class NodeReference extends Reference { - constructor(node: T, readonly moduleName: string|null) { super(node); } - - toExpression(context: ts.SourceFile): null { return null; } - - addIdentifier(identifier: ts.Identifier): void {} -} - -/** - * A reference to a node which has a `ts.Identifier` and can be resolved to an `Expression`. - * - * Imports generated by `ResolvedReference`s are always relative. - */ -export class ResolvedReference extends Reference { - protected identifiers: ts.Identifier[] = []; - - constructor(node: T, protected primaryIdentifier: ts.Identifier) { super(node); } - - readonly expressable = true; - - toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport): - Expression { - const localIdentifier = - pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode); - if (localIdentifier !== null) { - return new WrappedNodeExpr(localIdentifier); - } else { - // Relative import from context -> this.node.getSourceFile(). - // TODO(alxhub): investigate the impact of multiple source roots here. - // TODO(alxhub): investigate the need to map such paths via the Host for proper g3 support. - let relative = - path.posix.relative(path.dirname(context.fileName), this.node.getSourceFile().fileName) - .replace(TS_DTS_JS_EXTENSION, ''); - - // path.relative() does not include the leading './'. - if (!relative.startsWith('.')) { - relative = `./${relative}`; - } - - // path.relative() returns the empty string (converted to './' above) if the two paths are the - // same. - if (relative === './') { - // Same file after all. - return new WrappedNodeExpr(this.primaryIdentifier); - } else { - return new ExternalExpr(new ExternalReference(relative, this.primaryIdentifier.text)); - } - } - } - - addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); } -} - -/** - * A reference to a node which has a `ts.Identifer` and an expected absolute module name. - * - * An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import - * the module specifier will be an absolute module name, not a relative path. - */ -export class AbsoluteReference extends Reference { private identifiers: ts.Identifier[] = []; - constructor( - node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string, - readonly symbolName: string) { - super(node); - } - readonly expressable = true; + constructor(readonly node: T, bestGuessOwningModule: OwningModule|null = null) { + this.bestGuessOwningModule = bestGuessOwningModule; - toExpression(context: ts.SourceFile, importMode: ImportMode = ImportMode.UseExistingImport): - Expression { - const localIdentifier = - pickIdentifier(context, this.primaryIdentifier, this.identifiers, importMode); - if (localIdentifier !== null) { - return new WrappedNodeExpr(localIdentifier); - } else { - return new ExternalExpr(new ExternalReference(this.moduleName, this.symbolName)); + const id = identifierOfNode(node); + if (id !== null) { + this.identifiers.push(id); } } - addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); } -} - -function pickIdentifier( - context: ts.SourceFile, primary: ts.Identifier, secondaries: ts.Identifier[], - mode: ImportMode): ts.Identifier|null { - context = ts.getOriginalNode(context) as ts.SourceFile; - - if (ts.getOriginalNode(primary).getSourceFile() === context) { - return primary; - } else if (mode === ImportMode.UseExistingImport) { - return secondaries.find(id => ts.getOriginalNode(id).getSourceFile() === context) || null; - } else { - return null; + /** + * The best guess at which module specifier owns this particular reference, or `null` if there + * isn't one. + */ + get ownedByModuleGuess(): string|null { + if (this.bestGuessOwningModule !== null) { + return this.bestGuessOwningModule.specifier; + } else { + return null; + } } -} \ No newline at end of file + + /** + * Whether this reference has a potential owning module or not. + * + * See `bestGuessOwningModule`. + */ + get hasOwningModuleGuess(): boolean { return this.bestGuessOwningModule !== null; } + + /** + * A name for the node, if one is available. + * + * This is only suited for debugging. Any actual references to this node should be made with + * `ts.Identifier`s (see `getIdentityIn`). + */ + get debugName(): string|null { + const id = identifierOfNode(this.node); + return id !== null ? id.text : null; + } + + /** + * Record a `ts.Identifier` by which it's valid to refer to this node, within the context of this + * `Reference`. + */ + addIdentifier(identifier: ts.Identifier): void { this.identifiers.push(identifier); } + + /** + * Get a `ts.Identifier` within this `Reference` that can be used to refer within the context of a + * given `ts.SourceFile`, if any. + */ + getIdentityIn(context: ts.SourceFile): ts.Identifier|null { + return this.identifiers.find(id => id.getSourceFile() === context) || null; + } +} diff --git a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts index fbca9427ec..6fd244555a 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts @@ -8,9 +8,7 @@ import * as ts from 'typescript'; -import {isFromDtsFile} from '../../util/src/typescript'; - -import {AbsoluteReference, Reference, ResolvedReference} from './references'; +import {Reference} from './references'; export interface ReferenceResolver { resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string): @@ -38,101 +36,3 @@ export class ModuleResolver { return this.program.getSourceFile(resolved.resolvedFileName) || null; } } - -export class TsReferenceResolver implements ReferenceResolver { - private moduleExportsCache = new Map|null>(); - - constructor( - private program: ts.Program, private checker: ts.TypeChecker, - private options: ts.CompilerOptions, private host: ts.CompilerHost) {} - - resolve(decl: ts.Declaration, importFromHint: string|null, fromFile: string): - Reference { - const id = identifierOfDeclaration(decl); - if (id === undefined) { - throw new Error(`Internal error: don't know how to refer to ${ts.SyntaxKind[decl.kind]}`); - } - - if (!isFromDtsFile(decl) || importFromHint === null) { - return new ResolvedReference(decl, id); - } else { - const publicName = this.resolveImportName(importFromHint, decl, fromFile); - if (publicName !== null) { - return new AbsoluteReference(decl, id, importFromHint, publicName); - } else { - throw new Error(`Internal error: Symbol ${id.text} is not exported from ${importFromHint}`); - } - } - } - - private resolveImportName(moduleName: string, target: ts.Declaration, fromFile: string): string - |null { - const exports = this.getExportsOfModule(moduleName, fromFile); - if (exports !== null && exports.has(target)) { - return exports.get(target) !; - } else { - return null; - } - } - - private getExportsOfModule(moduleName: string, fromFile: string): - Map|null { - if (!this.moduleExportsCache.has(moduleName)) { - this.moduleExportsCache.set(moduleName, this.enumerateExportsOfModule(moduleName, fromFile)); - } - return this.moduleExportsCache.get(moduleName) !; - } - - private enumerateExportsOfModule(moduleName: string, fromFile: string): - Map|null { - const resolved = ts.resolveModuleName(moduleName, fromFile, this.options, this.host); - if (resolved.resolvedModule === undefined) { - return null; - } - - const indexFile = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName); - if (indexFile === undefined) { - return null; - } - - const indexSymbol = this.checker.getSymbolAtLocation(indexFile); - if (indexSymbol === undefined) { - return null; - } - - const exportMap = new Map(); - - const exports = this.checker.getExportsOfModule(indexSymbol); - for (const expSymbol of exports) { - const declSymbol = expSymbol.flags & ts.SymbolFlags.Alias ? - this.checker.getAliasedSymbol(expSymbol) : - expSymbol; - const decl = declSymbol.valueDeclaration; - if (decl === undefined) { - continue; - } - - if (declSymbol.name === expSymbol.name || !exportMap.has(decl)) { - exportMap.set(decl, expSymbol.name); - } - } - - return exportMap; - } -} - -function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined { - if (ts.isClassDeclaration(decl)) { - return decl.name; - } else if (ts.isEnumDeclaration(decl)) { - return decl.name; - } else if (ts.isFunctionDeclaration(decl)) { - return decl.name; - } else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) { - return decl.name; - } else if (ts.isShorthandPropertyAssignment(decl)) { - return decl.name; - } else { - return undefined; - } -} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts index 3f88ab1fd6..28617d1ca1 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {Reference, ReferenceResolver} from '../../imports'; +import {Reference} from '../../imports'; import {ReflectionHost} from '../../reflection'; import {StaticInterpreter} from './interpreter'; @@ -19,12 +19,10 @@ export type ForeignFunctionResolver = args: ReadonlyArray) => ts.Expression | null; export class PartialEvaluator { - constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private refResolver: ReferenceResolver) {} + constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { - const interpreter = new StaticInterpreter(this.host, this.checker, this.refResolver); + const interpreter = new StaticInterpreter(this.host, this.checker); return interpreter.visit(expr, { absoluteModuleName: null, resolutionContext: expr.getSourceFile().fileName, diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index b172def528..8a3f662780 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -8,7 +8,8 @@ import * as ts from 'typescript'; -import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports'; +import {Reference} from '../../imports'; +import {OwningModule} from '../../imports/src/references'; import {Declaration, ReflectionHost} from '../../reflection'; import {ArraySliceBuiltinFn} from './builtin'; @@ -78,9 +79,7 @@ interface Context { } export class StaticInterpreter { - constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private refResolver: ReferenceResolver) {} + constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {} visit(node: ts.Expression, context: Context): ResolvedValue { return this.visitExpression(node, context); @@ -332,10 +331,7 @@ export class StaticInterpreter { } else if (lhs instanceof Reference) { const ref = lhs.node; if (this.host.isClass(ref)) { - let absoluteModuleName = context.absoluteModuleName; - if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { - absoluteModuleName = lhs.moduleName || absoluteModuleName; - } + const module = owningModule(context, lhs.bestGuessOwningModule); let value: ResolvedValue = undefined; const member = this.host.getMembersOfClass(ref).find( member => member.isStatic && member.name === strIndex); @@ -343,9 +339,9 @@ export class StaticInterpreter { if (member.value !== null) { value = this.visitExpression(member.value, context); } else if (member.implementation !== null) { - value = new NodeReference(member.implementation, absoluteModuleName); + value = new Reference(member.implementation, module); } else if (member.node) { - value = new NodeReference(member.node, absoluteModuleName); + value = new Reference(member.node, module); } } return value; @@ -391,11 +387,10 @@ export class StaticInterpreter { // If the function is declared in a different file, resolve the foreign function expression // using the absolute module name of that file (if any). - if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) && - lhs.moduleName !== null) { + if (lhs.bestGuessOwningModule !== null) { context = { ...context, - absoluteModuleName: lhs.moduleName, + absoluteModuleName: lhs.bestGuessOwningModule.specifier, resolutionContext: node.getSourceFile().fileName, }; } @@ -496,7 +491,7 @@ export class StaticInterpreter { } private getReference(node: ts.Declaration, context: Context): Reference { - return this.refResolver.resolve(node, context.absoluteModuleName, context.resolutionContext); + return new Reference(node, owningModule(context)); } } @@ -542,3 +537,18 @@ function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): return EMPTY; } } + +function owningModule(context: Context, override: OwningModule | null = null): OwningModule|null { + let specifier = context.absoluteModuleName; + if (override !== null) { + specifier = override.specifier; + } + if (specifier !== null) { + return { + specifier, + resolutionContext: context.resolutionContext, + }; + } else { + return null; + } +} diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index e45057aba9..640c332b31 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -6,10 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; -import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports'; +import {Reference} from '../../imports'; import {TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {PartialEvaluator} from '../src/interface'; @@ -44,8 +43,7 @@ function evaluate( code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T { const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles); const reflectionHost = new TypeScriptReflectionHost(checker); - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); return evaluator.evaluate(expression) as T; } @@ -150,22 +148,17 @@ describe('ngtsc metadata', () => { const reflectionHost = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const resolved = evaluator.evaluate(expr); if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to a reference'); } expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - expect(resolved.expressable).toBe(true); - const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); - if (!(reference instanceof WrappedNodeExpr)) { - return fail('Expected expression reference to be a wrapped node'); + const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); + if (reference === null) { + return fail('Expected to get an identifier'); } - if (!ts.isIdentifier(reference.node)) { - return fail('Expected expression to be an Identifier'); - } - expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); + expect(reference.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); }); it('absolute imports work', () => { @@ -183,23 +176,16 @@ describe('ngtsc metadata', () => { const reflectionHost = new TypeScriptReflectionHost(checker); const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration); const expr = result.initializer !; - const resolver = new TsReferenceResolver(program, checker, options, host); - const evaluator = new PartialEvaluator(reflectionHost, checker, resolver); + const evaluator = new PartialEvaluator(reflectionHost, checker); const resolved = evaluator.evaluate(expr); - if (!(resolved instanceof AbsoluteReference)) { + if (!(resolved instanceof Reference)) { return fail('Expected expression to resolve to an absolute reference'); } - expect(resolved.moduleName).toBe('some_library'); + expect(owningModuleOf(resolved)).toBe('some_library'); expect(ts.isFunctionDeclaration(resolved.node)).toBe(true); - expect(resolved.expressable).toBe(true); - const reference = resolved.toExpression(program.getSourceFile('entry.ts') !); - if (!(reference instanceof WrappedNodeExpr)) { - return fail('Expected expression reference to be a wrapped node'); - } - if (!ts.isIdentifier(reference.node)) { - return fail('Expected expression to be an Identifier'); - } - expect(reference.node.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); + const reference = resolved.getIdentityIn(program.getSourceFile('entry.ts') !); + expect(reference).not.toBeNull(); + expect(reference !.getSourceFile()).toEqual(program.getSourceFile('entry.ts') !); }); it('reads values from default exports', () => { @@ -282,3 +268,7 @@ describe('ngtsc metadata', () => { expect(value instanceof Reference).toBe(true); }); }); + +function owningModuleOf(ref: Reference): string|null { + return ref.bestGuessOwningModule !== null ? ref.bestGuessOwningModule.specifier : null; +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 2906e9fd3e..aa39cfc5b7 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -17,8 +17,10 @@ import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {CycleAnalyzer, ImportGraph} from './cycles'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; -import {ImportRewriter, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports'; +import {AbsoluteModuleStrategy, FileToModuleHost, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; +import {FileToModuleStrategy} from './imports/src/emitter'; import {PartialEvaluator} from './partial_evaluator'; +import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; @@ -27,7 +29,7 @@ import {ivySwitchTransform} from './switch'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; import {TypeCheckContext, TypeCheckProgramHost} from './typecheck'; import {normalizeSeparators} from './util/src/path'; -import {isDtsPath} from './util/src/typescript'; +import {getRootDirs, isDtsPath} from './util/src/typescript'; export class NgtscProgram implements api.Program { private tsProgram: ts.Program; @@ -40,7 +42,7 @@ export class NgtscProgram implements api.Program { private _importRewriter: ImportRewriter|undefined = undefined; private _reflector: TypeScriptReflectionHost|undefined = undefined; private _isCore: boolean|undefined = undefined; - private rootDirs: string[]; + private rootDirs: AbsoluteFsPath[]; private closureCompilerEnabled: boolean; private entryPoint: ts.SourceFile|null; private exportReferenceGraph: ReferenceGraph|null = null; @@ -51,22 +53,20 @@ export class NgtscProgram implements api.Program { private moduleResolver: ModuleResolver; private cycleAnalyzer: CycleAnalyzer; + private refEmitter: ReferenceEmitter|null = null; + private fileToModuleHost: FileToModuleHost|null = null; constructor( rootNames: ReadonlyArray, private options: api.CompilerOptions, host: api.CompilerHost, oldProgram?: api.Program) { - this.rootDirs = []; - if (options.rootDirs !== undefined) { - this.rootDirs.push(...options.rootDirs); - } else if (options.rootDir !== undefined) { - this.rootDirs.push(options.rootDir); - } else { - this.rootDirs.push(host.getCurrentDirectory()); - } + this.rootDirs = getRootDirs(host, options); this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.resourceManager = new HostResourceLoader(host, options); const shouldGenerateShims = options.allowEmptyCodegenFiles || false; this.host = host; + if (host.fileNameToModuleName !== undefined) { + this.fileToModuleHost = host as FileToModuleHost; + } let rootFiles = [...rootNames]; const generators: ShimGenerator[] = []; @@ -168,7 +168,7 @@ export class NgtscProgram implements api.Program { const compilation = this.ensureAnalyzed(); const diagnostics = [...compilation.diagnostics]; if (!!this.options.fullTemplateTypeCheck) { - const ctx = new TypeCheckContext(); + const ctx = new TypeCheckContext(this.refEmitter !); compilation.typeCheck(ctx); diagnostics.push(...this.compileTypeCheckProgram(ctx)); } @@ -315,9 +315,32 @@ export class NgtscProgram implements api.Program { private makeCompilation(): IvyCompilation { const checker = this.tsProgram.getTypeChecker(); - const refResolver = new TsReferenceResolver(this.tsProgram, checker, this.options, this.host); - const evaluator = new PartialEvaluator(this.reflector, checker, refResolver); - const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, refResolver); + // Construct the ReferenceEmitter. + if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) { + // The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference + // resolution strategy. + this.refEmitter = new ReferenceEmitter([ + // First, try to use local identifiers if available. + new LocalIdentifierStrategy(), + // Next, attempt to use an absolute import. + new AbsoluteModuleStrategy(this.tsProgram, checker, this.options, this.host), + // Finally, check if the reference is being written into a file within the project's logical + // file system, and use a relative import if so. If this fails, ReferenceEmitter will throw + // an error. + new LogicalProjectStrategy(checker, new LogicalFileSystem(this.rootDirs)), + ]); + } else { + // The CompilerHost supports fileNameToModuleName, so use that to emit imports. + this.refEmitter = new ReferenceEmitter([ + // First, try to use local identifiers if available. + new LocalIdentifierStrategy(), + // Then use fileNameToModuleName to emit imports. + new FileToModuleStrategy(checker, this.fileToModuleHost), + ]); + } + + const evaluator = new PartialEvaluator(this.reflector, checker); + const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, this.refEmitter); // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there @@ -344,7 +367,7 @@ export class NgtscProgram implements api.Program { this.reflector, this.isCore, this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore, - this.routeAnalyzer), + this.routeAnalyzer, this.refEmitter), new PipeDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), ]; diff --git a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts index 3bfa9d2004..3e1eb580c3 100644 --- a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts +++ b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; -import {AbsoluteReference, NodeReference, Reference} from '../../imports'; +import {Reference} from '../../imports'; import {ForeignFunctionResolver, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {NgModuleRawRouteData} from './analyzer'; @@ -163,7 +163,9 @@ const routerModuleFFR: ForeignFunctionResolver = null { if (!isMethodNodeReference(ref) || !ts.isClassDeclaration(ref.node.parent)) { return null; - } else if (ref.moduleName !== '@angular/router') { + } else if ( + ref.bestGuessOwningModule === null || + ref.bestGuessOwningModule.specifier !== '@angular/router') { return null; } else if ( ref.node.parent.name === undefined || ref.node.parent.name.text !== 'RouterModule') { @@ -188,11 +190,11 @@ function hasIdentifier(node: ts.Node): node is ts.Node&{name: ts.Identifier} { function isMethodNodeReference( ref: Reference): - ref is NodeReference { - return ref instanceof NodeReference && ts.isMethodDeclaration(ref.node); + ref is Reference { + return ts.isMethodDeclaration(ref.node); } function isRouteToken(ref: ResolvedValue): boolean { - return ref instanceof AbsoluteReference && ref.moduleName === '@angular/router' && - ref.symbolName === 'ROUTES'; + return ref instanceof Reference && ref.bestGuessOwningModule !== null && + ref.bestGuessOwningModule.specifier === '@angular/router' && ref.debugName === 'ROUTES'; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index b76a88f9b2..43500d5955 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -9,7 +9,7 @@ import {R3TargetBinder, SelectorMatcher, TmplAstNode} from '@angular/compiler'; import * as ts from 'typescript'; -import {NoopImportRewriter} from '../../imports'; +import {NoopImportRewriter, ReferenceEmitter} from '../../imports'; import {ImportManager} from '../../translator'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCtorMetadata} from './api'; @@ -26,6 +26,8 @@ import {generateTypeCtor} from './type_constructor'; * checking code. */ export class TypeCheckContext { + constructor(private refEmitter: ReferenceEmitter) {} + /** * A `Set` of classes which will be used to generate type constructors. */ @@ -136,7 +138,7 @@ export class TypeCheckContext { // Process each operation and use the printer to generate source code for it, inserting it into // the source code in between the original chunks. ops.forEach((op, idx) => { - const text = op.execute(importManager, sf, printer); + const text = op.execute(importManager, sf, this.refEmitter, printer); code += text + textParts[idx + 1]; }); @@ -182,7 +184,8 @@ interface Op { /** * Execute the operation and return the generated code as text. */ - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string; + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string; } /** @@ -196,8 +199,9 @@ class TcbOp implements Op { */ get splitPoint(): number { return this.node.end + 1; } - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string { - const tcb = generateTypeCheckBlock(this.node, this.meta, im); + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string { + const tcb = generateTypeCheckBlock(this.node, this.meta, im, refEmitter); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); } } @@ -213,7 +217,8 @@ class TypeCtorOp implements Op { */ get splitPoint(): number { return this.node.end - 1; } - execute(im: ImportManager, sf: ts.SourceFile, printer: ts.Printer): string { + execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): + string { const tcb = generateTypeCtor(this.node, this.meta); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 08d207f5ac..3dfcc90f13 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -9,7 +9,7 @@ import {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; import * as ts from 'typescript'; -import {Reference} from '../../imports'; +import {Reference, ReferenceEmitter} from '../../imports'; import {ImportManager, translateExpression} from '../../translator'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api'; @@ -28,9 +28,9 @@ import {astToTypescript} from './expression'; * @param importManager an `ImportManager` for the file into which the TCB will be written. */ export function generateTypeCheckBlock( - node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, - importManager: ImportManager): ts.FunctionDeclaration { - const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager); + node: ts.ClassDeclaration, meta: TypeCheckBlockMetadata, importManager: ImportManager, + refEmitter: ReferenceEmitter): ts.FunctionDeclaration { + const tcb = new Context(meta.boundTarget, node.getSourceFile(), importManager, refEmitter); const scope = new Scope(tcb); tcbProcessNodes(meta.boundTarget.target.template !, tcb, scope); @@ -59,7 +59,8 @@ class Context { constructor( readonly boundTarget: BoundTarget, - private sourceFile: ts.SourceFile, private importManager: ImportManager) {} + private sourceFile: ts.SourceFile, private importManager: ImportManager, + private refEmitter: ReferenceEmitter) {} /** * Allocate a new variable name for use within the `Context`. @@ -75,7 +76,7 @@ class Context { * This may involve importing the node into the file if it's not declared there already. */ reference(ref: Reference): ts.Expression { - const ngExpr = ref.toExpression(this.sourceFile); + const ngExpr = this.refEmitter.emit(ref, this.sourceFile); if (ngExpr === null) { throw new Error(`Unreachable reference: ${ref.node}`); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel index 97da7159e6..233796c5ff 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel @@ -10,8 +10,11 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/util", "@ngdeps//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 29779fbb44..e92a7a8840 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -8,7 +8,10 @@ import * as ts from 'typescript'; +import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitter} from '../../imports'; +import {LogicalFileSystem} from '../../path'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; +import {getRootDirs} from '../../util/src/typescript'; import {TypeCheckContext} from '../src/context'; import {TypeCheckProgramHost} from '../src/host'; @@ -23,7 +26,6 @@ const LIB_D_TS = { describe('ngtsc typechecking', () => { describe('ctors', () => { it('compiles a basic type constructor', () => { - const ctx = new TypeCheckContext(); const files = [ LIB_D_TS, { name: 'main.ts', @@ -36,7 +38,15 @@ TestClass.ngTypeCtor({value: 'test'}); ` } ]; - const {program, host} = makeProgram(files, undefined, undefined, false); + const {program, host, options} = makeProgram(files, undefined, undefined, false); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem(getRootDirs(host, options)); + const emitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new AbsoluteModuleStrategy(program, checker, options, host), + new LogicalProjectStrategy(checker, logicalFs), + ]); + const ctx = new TypeCheckContext(emitter); const TestClass = getDeclaration(program, 'main.ts', 'TestClass', ts.isClassDeclaration); ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, { fnName: 'ngTypeCtor', diff --git a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel index 1f63dd3f2e..4f385af484 100644 --- a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel @@ -10,6 +10,7 @@ ts_library( ]), deps = [ "//packages:types", + "//packages/compiler-cli/src/ngtsc/path", "@ngdeps//@types/node", "@ngdeps//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index 3ef6e50cab..db4860a429 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -10,6 +10,7 @@ const TS = /\.tsx?$/i; const D_TS = /\.d\.ts$/i; import * as ts from 'typescript'; +import {AbsoluteFsPath} from '../../path'; export function isDtsPath(filePath: string): boolean { return D_TS.test(filePath); @@ -27,6 +28,17 @@ export function isFromDtsFile(node: ts.Node): boolean { return sf !== undefined && D_TS.test(sf.fileName); } +export function nodeNameForError(node: ts.Node & {name?: ts.Node}): string { + if (node.name !== undefined && ts.isIdentifier(node.name)) { + return node.name.text; + } else { + const kind = ts.SyntaxKind[node.kind]; + const {line, character} = + ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); + return `${kind}@${line}:${character}`; + } +} + export function getSourceFile(node: ts.Node): ts.SourceFile { // In certain transformation contexts, `ts.Node.getSourceFile()` can actually return `undefined`, // despite the type signature not allowing it. In that event, get the `ts.SourceFile` via the @@ -34,3 +46,37 @@ export function getSourceFile(node: ts.Node): ts.SourceFile { const directSf = node.getSourceFile() as ts.SourceFile | undefined; return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile(); } + +export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null { + if (decl.name !== undefined && ts.isIdentifier(decl.name)) { + return decl.name; + } else { + return null; + } +} + +export function isDeclaration(node: ts.Node): node is ts.Declaration { + return false || ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) || + ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node); +} + +export function isExported(node: ts.Declaration): boolean { + let topLevel: ts.Node = node; + if (ts.isVariableDeclaration(node) && ts.isVariableDeclarationList(node.parent)) { + topLevel = node.parent.parent; + } + return topLevel.modifiers !== undefined && + topLevel.modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword); +} + +export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions): AbsoluteFsPath[] { + const rootDirs: string[] = []; + if (options.rootDirs !== undefined) { + rootDirs.push(...options.rootDirs); + } else if (options.rootDir !== undefined) { + rootDirs.push(options.rootDir); + } else { + rootDirs.push(host.getCurrentDirectory()); + } + return rootDirs.map(rootDir => AbsoluteFsPath.fromUnchecked(rootDir)); +} diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 1ca98f4034..f0c553d229 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -112,6 +112,12 @@ export interface CompilerOptions extends ts.CompilerOptions { // This will be true be default in Angular 6. fullTemplateTypeCheck?: boolean; + // Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate + // import module specifiers. This is false by default, and exists to support running ngtsc + // within Google. This option is internal and is used by the ng_module.bzl rule to switch + // behavior between Bazel and Blaze. + _useHostForImportGeneration?: boolean; + // Insert JSDoc type annotations needed by Closure Compiler annotateForClosureCompiler?: boolean;