feat(compiler-cli): ngcc - make logging more configurable (#29591)

This allows CLI usage to filter excessive log messages
and integrations like webpack plugins to provide their own logger.

// FW-1198

PR Close #29591
This commit is contained in:
Pete Bacon Darwin
2019-03-29 10:13:14 +00:00
committed by Jason Aden
parent 39345b6fae
commit 8d3d75e454
31 changed files with 544 additions and 311 deletions

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
@ -49,7 +50,9 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
*/
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
protected dtsDeclarationMap: Map<string, ts.Declaration>|null;
constructor(protected isCore: boolean, checker: ts.TypeChecker, dts?: BundleProgram|null) {
constructor(
protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker,
dts?: BundleProgram|null) {
super(checker);
this.dtsDeclarationMap = dts && this.computeDtsDeclarationMap(dts.path, dts.program) || null;
}
@ -848,7 +851,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}
if (kind === null) {
console.warn(`Unknown member type: "${node.getText()}`);
this.logger.warn(`Unknown member type: "${node.getText()}`);
return null;
}

View File

@ -0,0 +1,46 @@
/**
* @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 {Logger} from './logger';
const RESET = '\x1b[0m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[36m';
export const DEBUG = `${BLUE}Debug:${RESET}`;
export const WARN = `${YELLOW}Warning:${RESET}`;
export const ERROR = `${RED}Error:${RESET}`;
export enum LogLevel {
debug,
info,
warn,
error,
}
/**
* A simple logger that outputs directly to the Console.
*
* The log messages can be filtered based on severity via the `logLevel`
* constructor parameter.
*/
export class ConsoleLogger implements Logger {
constructor(private logLevel: LogLevel) {}
debug(...args: string[]) {
if (this.logLevel <= LogLevel.debug) console.debug(DEBUG, ...args);
}
info(...args: string[]) {
if (this.logLevel <= LogLevel.info) console.info(...args);
}
warn(...args: string[]) {
if (this.logLevel <= LogLevel.warn) console.warn(WARN, ...args);
}
error(...args: string[]) {
if (this.logLevel <= LogLevel.error) console.error(ERROR, ...args);
}
}

View File

@ -0,0 +1,18 @@
/**
* @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
*/
/**
* Implement this interface if you want to provide different logging
* output from the standard ConsoleLogger.
*/
export interface Logger {
debug(...args: string[]): void;
info(...args: string[]): void;
warn(...args: string[]): void;
error(...args: string[]): void;
}

View File

@ -11,6 +11,8 @@ import {readFileSync} from 'fs';
import {AbsoluteFsPath} from '../../src/ngtsc/path';
import {ConsoleLogger, LogLevel} from './logging/console_logger';
import {Logger} from './logging/logger';
import {hasBeenProcessed, markAsProcessed} from './packages/build_marker';
import {DependencyHost} from './packages/dependency_host';
import {DependencyResolver} from './packages/dependency_resolver';
@ -23,6 +25,7 @@ import {InPlaceFileWriter} from './writing/in_place_file_writer';
import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer';
/**
* The options to configure the ngcc compiler.
*/
@ -50,6 +53,10 @@ export interface NgccOptions {
* Whether to create new entry-points bundles rather than overwriting the original files.
*/
createNewEntryPointFormats?: boolean;
/**
* Provide a logger that will be called with log messages.
*/
logger?: Logger;
}
const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015'];
@ -62,13 +69,14 @@ const SUPPORTED_FORMATS: EntryPointFormat[] = ['esm5', 'esm2015'];
*
* @param options The options telling ngcc what to compile and how.
*/
export function mainNgcc(
{basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
compileAllFormats = true, createNewEntryPointFormats = false}: NgccOptions): void {
const transformer = new Transformer(basePath);
export function mainNgcc({basePath, targetEntryPointPath,
propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES,
compileAllFormats = true, createNewEntryPointFormats = false,
logger = new ConsoleLogger(LogLevel.info)}: NgccOptions): void {
const transformer = new Transformer(logger, basePath);
const host = new DependencyHost();
const resolver = new DependencyResolver(host);
const finder = new EntryPointFinder(resolver);
const resolver = new DependencyResolver(logger, host);
const finder = new EntryPointFinder(logger, resolver);
const fileWriter = getFileWriter(createNewEntryPointFormats);
const absoluteTargetEntryPointPath = targetEntryPointPath ?
@ -112,7 +120,7 @@ export function mainNgcc(
if (hasBeenProcessed(entryPointPackageJson, property)) {
compiledFormats.add(formatPath);
console.warn(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
logger.info(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
continue;
}
@ -123,16 +131,16 @@ export function mainNgcc(
entryPoint.path, formatPath, entryPoint.typings, isCore, property, format,
compiledFormats.size === 0);
if (bundle) {
console.warn(`Compiling ${entryPoint.name} : ${property} as ${format}`);
logger.info(`Compiling ${entryPoint.name} : ${property} as ${format}`);
const transformedFiles = transformer.transform(bundle);
fileWriter.writeBundle(entryPoint, bundle, transformedFiles);
compiledFormats.add(formatPath);
} else {
console.warn(
logger.warn(
`Skipping ${entryPoint.name} : ${format} (no valid entry point file for this format).`);
}
} else if (!compileAllFormats) {
console.warn(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
logger.info(`Skipping ${entryPoint.name} : ${property} (already compiled).`);
}
// Either this format was just compiled or its underlying format was compiled because of a

View File

@ -10,6 +10,8 @@ import {resolve} from 'canonical-path';
import {DepGraph} from 'dependency-graph';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Logger} from '../logging/logger';
import {DependencyHost} from './dependency_host';
import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat} from './entry_point';
@ -65,7 +67,7 @@ export interface SortedEntryPointsInfo {
* A class that resolves dependencies between entry-points.
*/
export class DependencyResolver {
constructor(private host: DependencyHost) {}
constructor(private logger: Logger, private host: DependencyHost) {}
/**
* Sort the array of entry points so that the dependant entry points always come later than
* their dependencies in the array.
@ -134,7 +136,7 @@ export class DependencyResolver {
if (deepImports.size) {
const imports = Array.from(deepImports).map(i => `'${i}'`).join(', ');
console.warn(
this.logger.warn(
`Entry point '${entryPoint.name}' contains deep imports into ${imports}. ` +
`This is probably not a problem, but may cause the compilation of entry points to be out of order.`);
}

View File

@ -10,6 +10,7 @@ import * as path from 'canonical-path';
import * as fs from 'fs';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Logger} from '../logging/logger';
/**
@ -67,13 +68,13 @@ export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] =
* @returns An entry-point if it is valid, `null` otherwise.
*/
export function getEntryPointInfo(
packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath): EntryPoint|null {
logger: Logger, packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath): EntryPoint|null {
const packageJsonPath = path.resolve(entryPointPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const entryPointPackageJson = loadEntryPointPackage(packageJsonPath);
const entryPointPackageJson = loadEntryPointPackage(logger, packageJsonPath);
if (!entryPointPackageJson) {
return null;
}
@ -135,12 +136,13 @@ export function getEntryPointFormat(property: string): EntryPointFormat|undefine
* @param packageJsonPath the absolute path to the package.json file.
* @returns JSON from the package.json file if it is valid, `null` otherwise.
*/
function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|null {
function loadEntryPointPackage(logger: Logger, packageJsonPath: string): EntryPointPackageJson|
null {
try {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
} catch (e) {
// We may have run into a package.json with unexpected symbols
console.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
logger.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
return null;
}
}

View File

@ -9,109 +9,111 @@ import * as path from 'canonical-path';
import * as fs from 'fs';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Logger} from '../logging/logger';
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
import {EntryPoint, getEntryPointInfo} from './entry_point';
export class EntryPointFinder {
constructor(private resolver: DependencyResolver) {}
constructor(private logger: Logger, private resolver: DependencyResolver) {}
/**
* Search the given directory, and sub-directories, for Angular package entry points.
* @param sourceDirectory An absolute path to the directory to search for entry points.
*/
findEntryPoints(sourceDirectory: AbsoluteFsPath, targetEntryPointPath?: AbsoluteFsPath):
SortedEntryPointsInfo {
const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory);
const unsortedEntryPoints = this.walkDirectoryForEntryPoints(sourceDirectory);
const targetEntryPoint = targetEntryPointPath ?
unsortedEntryPoints.find(entryPoint => entryPoint.path === targetEntryPointPath) :
undefined;
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints, targetEntryPoint);
}
}
/**
* Look for entry points that need to be compiled, starting at the source directory.
* The function will recurse into directories that start with `@...`, e.g. `@angular/...`.
* @param sourceDirectory An absolute path to the root directory where searching begins.
*/
function walkDirectoryForEntryPoints(sourceDirectory: AbsoluteFsPath): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
fs.readdirSync(sourceDirectory)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(sourceDirectory, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(p => {
// Either the directory is a potential package or a namespace containing packages (e.g
// `@angular`).
const packagePath = AbsoluteFsPath.from(path.join(sourceDirectory, p));
if (p.startsWith('@')) {
entryPoints.push(...walkDirectoryForEntryPoints(packagePath));
} else {
entryPoints.push(...getEntryPointsForPackage(packagePath));
/**
* Look for entry points that need to be compiled, starting at the source directory.
* The function will recurse into directories that start with `@...`, e.g. `@angular/...`.
* @param sourceDirectory An absolute path to the root directory where searching begins.
*/
private walkDirectoryForEntryPoints(sourceDirectory: AbsoluteFsPath): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
fs.readdirSync(sourceDirectory)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(sourceDirectory, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(p => {
// Either the directory is a potential package or a namespace containing packages (e.g
// `@angular`).
const packagePath = AbsoluteFsPath.from(path.join(sourceDirectory, p));
if (p.startsWith('@')) {
entryPoints.push(...this.walkDirectoryForEntryPoints(packagePath));
} else {
entryPoints.push(...this.getEntryPointsForPackage(packagePath));
// Also check for any nested node_modules in this package
const nestedNodeModulesPath =
AbsoluteFsPath.from(path.resolve(packagePath, 'node_modules'));
if (fs.existsSync(nestedNodeModulesPath)) {
entryPoints.push(...walkDirectoryForEntryPoints(nestedNodeModulesPath));
// Also check for any nested node_modules in this package
const nestedNodeModulesPath =
AbsoluteFsPath.from(path.resolve(packagePath, 'node_modules'));
if (fs.existsSync(nestedNodeModulesPath)) {
entryPoints.push(...this.walkDirectoryForEntryPoints(nestedNodeModulesPath));
}
}
}
});
return entryPoints;
}
/**
* Recurse the folder structure looking for all the entry points
* @param packagePath The absolute path to an npm package that may contain entry points
* @returns An array of entry points that were discovered.
*/
function getEntryPointsForPackage(packagePath: AbsoluteFsPath): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
// Try to get an entry point from the top level package directory
const topLevelEntryPoint = getEntryPointInfo(packagePath, packagePath);
if (topLevelEntryPoint !== null) {
entryPoints.push(topLevelEntryPoint);
});
return entryPoints;
}
// Now search all the directories of this package for possible entry points
walkDirectory(packagePath, subdir => {
const subEntryPoint = getEntryPointInfo(packagePath, subdir);
if (subEntryPoint !== null) {
entryPoints.push(subEntryPoint);
/**
* Recurse the folder structure looking for all the entry points
* @param packagePath The absolute path to an npm package that may contain entry points
* @returns An array of entry points that were discovered.
*/
private getEntryPointsForPackage(packagePath: AbsoluteFsPath): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
// Try to get an entry point from the top level package directory
const topLevelEntryPoint = getEntryPointInfo(this.logger, packagePath, packagePath);
if (topLevelEntryPoint !== null) {
entryPoints.push(topLevelEntryPoint);
}
});
return entryPoints;
}
// Now search all the directories of this package for possible entry points
this.walkDirectory(packagePath, subdir => {
const subEntryPoint = getEntryPointInfo(this.logger, packagePath, subdir);
if (subEntryPoint !== null) {
entryPoints.push(subEntryPoint);
}
});
/**
* Recursively walk a directory and its sub-directories, applying a given
* function to each directory.
* @param dir the directory to recursively walk.
* @param fn the function to apply to each directory.
*/
function walkDirectory(dir: AbsoluteFsPath, fn: (dir: AbsoluteFsPath) => void) {
return fs
.readdirSync(dir)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(dir, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(subDir => {
const resolvedSubDir = AbsoluteFsPath.from(path.resolve(dir, subDir));
fn(resolvedSubDir);
walkDirectory(resolvedSubDir, fn);
});
return entryPoints;
}
/**
* Recursively walk a directory and its sub-directories, applying a given
* function to each directory.
* @param dir the directory to recursively walk.
* @param fn the function to apply to each directory.
*/
private walkDirectory(dir: AbsoluteFsPath, fn: (dir: AbsoluteFsPath) => void) {
return fs
.readdirSync(dir)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(dir, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(subDir => {
const resolvedSubDir = AbsoluteFsPath.from(path.resolve(dir, subDir));
fn(resolvedSubDir);
this.walkDirectory(resolvedSubDir, fn);
});
}
}

View File

@ -15,6 +15,7 @@ import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_mar
import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Logger} from '../logging/logger';
import {Esm5Renderer} from '../rendering/esm5_renderer';
import {EsmRenderer} from '../rendering/esm_renderer';
import {FileInfo, Renderer} from '../rendering/renderer';
@ -45,7 +46,7 @@ import {EntryPointBundle} from './entry_point_bundle';
* - Some formats may contain multiple "modules" in a single file.
*/
export class Transformer {
constructor(private sourcePath: string) {}
constructor(private logger: Logger, private sourcePath: string) {}
/**
* Transform the source (and typings) files of a bundle.
@ -73,9 +74,9 @@ export class Transformer {
const typeChecker = bundle.src.program.getTypeChecker();
switch (bundle.format) {
case 'esm2015':
return new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
return new Esm2015ReflectionHost(this.logger, isCore, typeChecker, bundle.dts);
case 'esm5':
return new Esm5ReflectionHost(isCore, typeChecker, bundle.dts);
return new Esm5ReflectionHost(this.logger, isCore, typeChecker, bundle.dts);
default:
throw new Error(`Reflection host for "${bundle.format}" not yet implemented.`);
}
@ -84,9 +85,9 @@ export class Transformer {
getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer {
switch (bundle.format) {
case 'esm2015':
return new EsmRenderer(host, isCore, bundle, this.sourcePath);
return new EsmRenderer(this.logger, host, isCore, bundle, this.sourcePath);
case 'esm5':
return new Esm5Renderer(host, isCore, bundle, this.sourcePath);
return new Esm5Renderer(this.logger, host, isCore, bundle, this.sourcePath);
default:
throw new Error(`Renderer for "${bundle.format}" not yet implemented.`);
}

View File

@ -12,11 +12,13 @@ import {NgccReflectionHost} from '../host/ngcc_host';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {EsmRenderer} from './esm_renderer';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Logger} from '../logging/logger';
export class Esm5Renderer extends EsmRenderer {
constructor(
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string) {
super(host, isCore, bundle, sourcePath);
logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle,
sourcePath: string) {
super(logger, host, isCore, bundle, sourcePath);
}
/**

View File

@ -14,11 +14,13 @@ import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {Logger} from '../logging/logger';
export class EsmRenderer extends Renderer {
constructor(
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string) {
super(host, isCore, bundle, sourcePath);
logger: Logger, host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle,
sourcePath: string) {
super(logger, host, isCore, bundle, sourcePath);
}
/**

View File

@ -24,6 +24,7 @@ import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_mar
import {IMPORT_PREFIX} from '../constants';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Logger} from '../logging/logger';
interface SourceMapInfo {
source: string;
@ -80,7 +81,7 @@ export const RedundantDecoratorMap = Map;
*/
export abstract class Renderer {
constructor(
protected host: NgccReflectionHost, protected isCore: boolean,
protected logger: Logger, protected host: NgccReflectionHost, protected isCore: boolean,
protected bundle: EntryPointBundle, protected sourcePath: string) {}
renderProgram(
@ -299,16 +300,16 @@ export abstract class Renderer {
externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName));
} catch (e) {
if (e.code === 'ENOENT') {
console.warn(
this.logger.warn(
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
const mapPath = file.fileName + '.map';
if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) {
console.warn(
this.logger.warn(
`Guessing the map file name from the source file name: "${basename(mapPath)}"`);
try {
externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8')));
} catch (e) {
console.error(e);
this.logger.error(e);
}
}
}