Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/compiler-cli/ngcc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {EntryPointJsonProperty, EntryPointPackageJson} from './src/packages/entr
export {ConsoleLogger, LogLevel} from './src/logging/console_logger';
export {Logger} from './src/logging/logger';
export {NgccOptions, mainNgcc as process} from './src/main';
export {PathMappings} from './src/utils';

export function hasBeenProcessed(packageJson: object, format: string) {
// We are wrapping this function to hide the internal types.
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/ngcc/main-ngcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* 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 path from 'canonical-path';
import * as yargs from 'yargs';

import {AbsoluteFsPath} from '../src/ngtsc/path';
import {mainNgcc} from './src/main';
import {ConsoleLogger, LogLevel} from './src/logging/console_logger';

Expand Down Expand Up @@ -56,7 +56,7 @@ if (require.main === module) {
'The formats option (-f/--formats) has been removed. Consider the properties option (-p/--properties) instead.');
process.exit(1);
}
const baseSourcePath = path.resolve(options['s'] || './node_modules');
const baseSourcePath = AbsoluteFsPath.resolve(options['s'] || './node_modules');
const propertiesToConsider: string[] = options['p'];
const targetEntryPointPath = options['t'] ? options['t'] : undefined;
const compileAllFormats = !options['first-only'];
Expand Down
17 changes: 8 additions & 9 deletions packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool} from '@angular/compiler';
import {NOOP_PERF_RECORDER} from '@angular/compiler-cli/src/ngtsc/perf';
import * as path from 'canonical-path';
import * as fs from 'fs';
import * as ts from 'typescript';

import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
Expand All @@ -19,6 +16,7 @@ import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {FileSystem} from '../file_system/file_system';
import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
Expand Down Expand Up @@ -53,28 +51,29 @@ export interface MatchingHandler<A, M> {
* Simple class that resolves and loads files directly from the filesystem.
*/
class NgccResourceLoader implements ResourceLoader {
constructor(private fs: FileSystem) {}
canPreload = false;
preload(): undefined|Promise<void> { throw new Error('Not implemented.'); }
load(url: string): string { return fs.readFileSync(url, 'utf8'); }
load(url: string): string { return this.fs.readFile(AbsoluteFsPath.resolve(url)); }
resolve(url: string, containingFile: string): string {
return path.resolve(path.dirname(containingFile), url);
return AbsoluteFsPath.resolve(AbsoluteFsPath.dirname(AbsoluteFsPath.from(containingFile)), url);
}
}

/**
* This Analyzer will analyze the files that have decorated classes that need to be transformed.
*/
export class DecorationAnalyzer {
resourceManager = new NgccResourceLoader();
resourceManager = new NgccResourceLoader(this.fs);
metaRegistry = new LocalMetadataRegistry();
dtsMetaReader = new DtsMetadataReader(this.typeChecker, this.reflectionHost);
fullMetaReader = new CompoundMetadataReader([this.metaRegistry, this.dtsMetaReader]);
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.
// projects only ever have one rootDir. Instead, ngcc should just switch its emitted import
// based on whether a bestGuessOwningModule is present in the Reference.
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
]);
dtsModuleScopeResolver =
Expand Down Expand Up @@ -110,7 +109,7 @@ export class DecorationAnalyzer {
];

constructor(
private program: ts.Program, private options: ts.CompilerOptions,
private fs: FileSystem, private program: ts.Program, private options: ts.CompilerOptions,
private host: ts.CompilerHost, private typeChecker: ts.TypeChecker,
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
private rootDirs: AbsoluteFsPath[], private isCore: boolean) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@
*/
import * as ts from 'typescript';

import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Declaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils';
import {NgccReferencesRegistry} from './ngcc_references_registry';

export interface ExportInfo {
identifier: string;
from: string;
dtsFrom?: string|null;
from: AbsoluteFsPath;
dtsFrom?: AbsoluteFsPath|null;
alias?: string|null;
}
export type PrivateDeclarationsAnalyses = ExportInfo[];
Expand Down Expand Up @@ -93,11 +94,12 @@ export class PrivateDeclarationsAnalyzer {
});

return Array.from(privateDeclarations.keys()).map(id => {
const from = id.getSourceFile().fileName;
const from = AbsoluteFsPath.fromSourceFile(id.getSourceFile());
const declaration = privateDeclarations.get(id) !;
const alias = exportAliasDeclarations.get(id) || null;
const dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
const dtsFrom =
dtsDeclaration && AbsoluteFsPath.fromSourceFile(dtsDeclaration.getSourceFile());

return {identifier: id.text, from, dtsFrom, alias};
});
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler-cli/ngcc/src/dependencies/dependency_host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @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 {AbsoluteFsPath} from '../../../src/ngtsc/path';

export interface DependencyHost {
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo;
}

export interface DependencyInfo {
dependencies: Set<AbsoluteFsPath>;
missing: Set<string>;
deepImports: Set<string>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/

import {resolve} from 'canonical-path';
import {DepGraph} from 'dependency-graph';

import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Logger} from '../logging/logger';
import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from '../packages/entry_point';

import {DependencyHost} from './dependency_host';
import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from './entry_point';



/**
Expand Down Expand Up @@ -48,6 +48,11 @@ export interface IgnoredDependency {
dependencyPath: string;
}

export interface DependencyDiagnostics {
invalidEntryPoints: InvalidEntryPoint[];
ignoredDependencies: IgnoredDependency[];
}

/**
* A list of entry-points, sorted by their dependencies.
*
Expand All @@ -57,11 +62,7 @@ export interface IgnoredDependency {
* Some entry points or their dependencies may be have been ignored. These are captured for
* diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
*/
export interface SortedEntryPointsInfo {
entryPoints: EntryPoint[];
invalidEntryPoints: InvalidEntryPoint[];
ignoredDependencies: IgnoredDependency[];
}
export interface SortedEntryPointsInfo extends DependencyDiagnostics { entryPoints: EntryPoint[]; }

/**
* A class that resolves dependencies between entry-points.
Expand All @@ -77,12 +78,17 @@ export class DependencyResolver {
*/
sortEntryPointsByDependency(entryPoints: EntryPoint[], target?: EntryPoint):
SortedEntryPointsInfo {
const {invalidEntryPoints, ignoredDependencies, graph} = this.createDependencyInfo(entryPoints);
const {invalidEntryPoints, ignoredDependencies, graph} =
this.computeDependencyGraph(entryPoints);

let sortedEntryPointNodes: string[];
if (target) {
sortedEntryPointNodes = graph.dependenciesOf(target.path);
sortedEntryPointNodes.push(target.path);
if (target.compiledByAngular) {
sortedEntryPointNodes = graph.dependenciesOf(target.path);
sortedEntryPointNodes.push(target.path);
} else {
sortedEntryPointNodes = [];
}
} else {
sortedEntryPointNodes = graph.overallOrder();
}
Expand All @@ -100,18 +106,20 @@ export class DependencyResolver {
* The graph only holds entry-points that ngcc cares about and whose dependencies
* (direct and transitive) all exist.
*/
private createDependencyInfo(entryPoints: EntryPoint[]) {
private computeDependencyGraph(entryPoints: EntryPoint[]): DependencyGraph {
const invalidEntryPoints: InvalidEntryPoint[] = [];
const ignoredDependencies: IgnoredDependency[] = [];
const graph = new DepGraph<EntryPoint>();

// Add the entry points to the graph as nodes
entryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint));
const angularEntryPoints = entryPoints.filter(entryPoint => entryPoint.compiledByAngular);

// Add the Angular compiled entry points to the graph as nodes
angularEntryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint));

// Now add the dependencies between them
entryPoints.forEach(entryPoint => {
angularEntryPoints.forEach(entryPoint => {
const entryPointPath = getEntryPointPath(entryPoint);
const {dependencies, missing, deepImports} = this.host.computeDependencies(entryPointPath);
const {dependencies, missing, deepImports} = this.host.findDependencies(entryPointPath);

if (missing.size > 0) {
// This entry point has dependencies that are missing
Expand Down Expand Up @@ -162,8 +170,12 @@ function getEntryPointPath(entryPoint: EntryPoint): AbsoluteFsPath {

if (format === 'esm2015' || format === 'esm5') {
const formatPath = entryPoint.packageJson[property] !;
return AbsoluteFsPath.from(resolve(entryPoint.path, formatPath));
return AbsoluteFsPath.resolve(entryPoint.path, formatPath);
}
}
throw new Error(`There is no format with import statements in '${entryPoint.path}' entry-point.`);
}

interface DependencyGraph extends DependencyDiagnostics {
graph: DepGraph<EntryPoint>;
}
117 changes: 117 additions & 0 deletions packages/compiler-cli/ngcc/src/dependencies/esm_dependency_host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @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';

import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {DependencyHost, DependencyInfo} from './dependency_host';
import {ModuleResolver, ResolvedDeepImport, ResolvedRelativeModule} from './module_resolver';


/**
* Helper functions for computing dependencies.
*/
export class EsmDependencyHost implements DependencyHost {
constructor(private fs: FileSystem, private moduleResolver: ModuleResolver) {}

/**
* Find all the dependencies for the entry-point at the given path.
*
* @param entryPointPath The absolute path to the JavaScript file that represents an entry-point.
* @returns Information about the dependencies of the entry-point, including those that were
* missing or deep imports into other entry-points.
*/
findDependencies(entryPointPath: AbsoluteFsPath): DependencyInfo {
const dependencies = new Set<AbsoluteFsPath>();
const missing = new Set<string>();
const deepImports = new Set<string>();
const alreadySeen = new Set<AbsoluteFsPath>();
this.recursivelyFindDependencies(
entryPointPath, dependencies, missing, deepImports, alreadySeen);
return {dependencies, missing, deepImports};
}

/**
* Compute the dependencies of the given file.
*
* @param file An absolute path to the file whose dependencies we want to get.
* @param dependencies A set that will have the absolute paths of resolved entry points added to
* it.
* @param missing A set that will have the dependencies that could not be found added to it.
* @param deepImports A set that will have the import paths that exist but cannot be mapped to
* entry-points, i.e. deep-imports.
* @param alreadySeen A set that is used to track internal dependencies to prevent getting stuck
* in a
* circular dependency loop.
*/
private recursivelyFindDependencies(
file: AbsoluteFsPath, dependencies: Set<AbsoluteFsPath>, missing: Set<string>,
deepImports: Set<string>, alreadySeen: Set<AbsoluteFsPath>): void {
const fromContents = this.fs.readFile(file);
if (!this.hasImportOrReexportStatements(fromContents)) {
return;
}

// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(file, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
sf.statements
// filter out statements that are not imports or reexports
.filter(this.isStringImportOrReexport)
// Grab the id of the module that is being imported
.map(stmt => stmt.moduleSpecifier.text)
// Resolve this module id into an absolute path
.forEach(importPath => {
const resolvedModule = this.moduleResolver.resolveModuleImport(importPath, file);
if (resolvedModule) {
if (resolvedModule instanceof ResolvedRelativeModule) {
const internalDependency = resolvedModule.modulePath;
if (!alreadySeen.has(internalDependency)) {
alreadySeen.add(internalDependency);
this.recursivelyFindDependencies(
internalDependency, dependencies, missing, deepImports, alreadySeen);
}
} else {
if (resolvedModule instanceof ResolvedDeepImport) {
deepImports.add(resolvedModule.importPath);
} else {
dependencies.add(resolvedModule.entryPointPath);
}
}
} else {
missing.add(importPath);
}
});
}

/**
* Check whether the given statement is an import with a string literal module specifier.
* @param stmt the statement node to check.
* @returns true if the statement is an import with a string literal module specifier.
*/
isStringImportOrReexport(stmt: ts.Statement): stmt is ts.ImportDeclaration&
{moduleSpecifier: ts.StringLiteral} {
return ts.isImportDeclaration(stmt) ||
ts.isExportDeclaration(stmt) && !!stmt.moduleSpecifier &&
ts.isStringLiteral(stmt.moduleSpecifier);
}

/**
* Check whether a source file needs to be parsed for imports.
* This is a performance short-circuit, which saves us from creating
* a TypeScript AST unnecessarily.
*
* @param source The content of the source file to check.
*
* @returns false if there are definitely no import or re-export statements
* in this file, true otherwise.
*/
hasImportOrReexportStatements(source: string): boolean {
return /(import|export)\s.+from/.test(source);
}
}
Loading