Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-extractor] Remove ILogger and instead expose console output as ExtractorMessage objects #1211

Merged
merged 7 commits into from
Apr 6, 2019
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 3 additions & 3 deletions apps/api-extractor/src/analyzer/AstSymbolTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { TypeScriptHelpers } from './TypeScriptHelpers';
import { AstSymbol } from './AstSymbol';
import { AstModule, AstModuleExportInfo } from './AstModule';
import { PackageMetadataManager } from './PackageMetadataManager';
import { ILogger } from '../api/ILogger';
import { ExportAnalyzer } from './ExportAnalyzer';
import { AstImport } from './AstImport';
import { MessageRouter } from '../collector/MessageRouter';

export type AstEntity = AstSymbol | AstImport;

Expand Down Expand Up @@ -83,11 +83,11 @@ export class AstSymbolTable {
= new Map<ts.Identifier, AstEntity | undefined>();

public constructor(program: ts.Program, typeChecker: ts.TypeChecker, packageJsonLookup: PackageJsonLookup,
logger: ILogger) {
messageRouter: MessageRouter) {

this._program = program;
this._typeChecker = typeChecker;
this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, logger);
this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, messageRouter);

this._exportAnalyzer = new ExportAnalyzer(
this._program,
Expand Down
11 changes: 6 additions & 5 deletions apps/api-extractor/src/analyzer/PackageMetadataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
INodePackageJson
} from '@microsoft/node-core-library';
import { Extractor } from '../api/Extractor';
import { ILogger } from '../api/ILogger';
import { MessageRouter } from '../collector/MessageRouter';
import { ConsoleMessageId } from '../api/ConsoleMessageId';

/**
* Represents analyzed information for a package.json file.
Expand Down Expand Up @@ -57,7 +58,7 @@ export class PackageMetadataManager {
public static tsdocMetadataFilename: string = 'tsdoc-metadata.json';

private readonly _packageJsonLookup: PackageJsonLookup;
private readonly _logger: ILogger;
private readonly _messageRouter: MessageRouter;
private readonly _packageMetadataByPackageJsonPath: Map<string, PackageMetadata>
= new Map<string, PackageMetadata>();

Expand Down Expand Up @@ -147,9 +148,9 @@ export class PackageMetadataManager {
});
}

public constructor(packageJsonLookup: PackageJsonLookup, logger: ILogger) {
public constructor(packageJsonLookup: PackageJsonLookup, messageRouter: MessageRouter) {
this._packageJsonLookup = packageJsonLookup;
this._logger = logger;
this._messageRouter = messageRouter;
}

/**
Expand Down Expand Up @@ -179,7 +180,7 @@ export class PackageMetadataManager {
);

if (FileSystem.exists(tsdocMetadataPath)) {
this._logger.logVerbose('Found metadata in ' + tsdocMetadataPath);
this._messageRouter.logVerbose(ConsoleMessageId.FoundTSDocMetadata, 'Found metadata in ' + tsdocMetadataPath);
// If the file exists at all, assume it was written by API Extractor
aedocSupported = true;
}
Expand Down
50 changes: 50 additions & 0 deletions apps/api-extractor/src/api/ConsoleMessageId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/**
* Unique identifiers for console messages reported by API Extractor.
*
* @remarks
*
* These strings are possible values for the {@link ExtractorMessage.messageId} property
* when the `ExtractorMessage.category` is {@link ExtractorMessageCategory.Console}.
*
* @public
*/
export const enum ConsoleMessageId {
/**
* "'Found metadata in ___"
*/
FoundTSDocMetadata = 'console-found-tsdoc-metadata',

/**
* "Writing: ___"
*/
WritingDocModelFile = 'console-writing-doc-model-file',

/**
* "Writing package typings: ___"
*/
WritingDtsRollup = 'console-writing-dts-rollup',

/**
* "You have changed the public API signature for this project. Please overwrite ___ with a
* copy of ___ and then request an API review. See the Git repository README.md for more info."
*/
ApiReportNotCopied = 'console-api-report-not-copied',

/**
* "You have changed the public API signature for this project. Updating ___"
*/
ApiReportCopied = 'console-api-report-copied',

/**
* "The API signature is up to date: ___"
*/
ApiReportUnchanged = 'console-api-report-unchanged',

/**
* "The API review file has not been set up. Do this by copying ___ to ___ and committing it."
*/
ApiReportMissing = 'console-api-report-missing'
}
117 changes: 60 additions & 57 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import lodash = require('lodash');
import colors = require('colors');

import {
FileSystem,
NewlineKind,
PackageJsonLookup,
IPackageJson
} from '@microsoft/node-core-library';
import { ExtractorConfig } from './ExtractorConfig';
import { ILogger } from './ILogger';
import { Collector } from '../collector/Collector';
import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator';
import { MonitoredLogger } from './MonitoredLogger';
import { ApiModelGenerator } from '../generators/ApiModelGenerator';
import { ApiPackage } from '@microsoft/api-extractor-model';
import { ReviewFileGenerator } from '../generators/ReviewFileGenerator';
import { PackageMetadataManager } from '../analyzer/PackageMetadataManager';
import { ValidationEnhancer } from '../enhancers/ValidationEnhancer';
import { DocCommentEnhancer } from '../enhancers/DocCommentEnhancer';
import { CompilerState } from './CompilerState';
import { ExtractorMessage } from './ExtractorMessage';
import { MessageRouter } from '../collector/MessageRouter';
import { ConsoleMessageId } from './ConsoleMessageId';

/**
* Runtime options for Extractor.
Expand All @@ -35,12 +33,6 @@ export interface IExtractorInvokeOptions {
*/
compilerState?: CompilerState;

/**
* Allows the caller to customize how API Extractor's errors, warnings, and informational logging is processed.
* If omitted, the output will be printed to the console.
*/
customLogger?: Partial<ILogger>;

/**
* Indicates that API Extractor is running as part of a local build, e.g. on developer's
* machine. This disables certain validation that would normally be performed
Expand All @@ -51,12 +43,27 @@ export interface IExtractorInvokeOptions {
*/
localBuild?: boolean;

/**
* If true, API Extractor will include {@link ExtractorLogLevel.Verbose} messages in its output.
*/
showVerboseMessages?: boolean;

/**
* By default API Extractor uses its own TypeScript compiler version to analyze your project.
* This can often cause compiler errors due to incompatibilities between different TS versions.
* Use this option to specify the folder path for your compiler version.
*/
typescriptCompilerFolder?: string;

/**
* An optional callback function that will be called for each `ExtractorMessage` before it is displayed by
* API Extractor. The callback can customize the message, handle it, or discard it.
*
* @remarks
* If a `messageCallback` is not provided, then by default API Extractor will print the messages to
* the STDERR/STDOUT console.
*/
messageCallback?: (message: ExtractorMessage) => void;
}

/**
Expand Down Expand Up @@ -88,12 +95,23 @@ export class ExtractorResult {
public readonly succeeded: boolean;

/**
* Reports the number of times that {@link ILogger.logError} was called.
* Returns true if the API report was found to have changed.
*/
public readonly apiReportChanged: boolean;

/**
* Reports the number of errors encountered during analysis.
*
* @remarks
* This does not count exceptions, where unexpected issues prematurely abort the operation.
*/
public readonly errorCount: number;

/**
* Reports the number of times that {@link ILogger.logWarning} was called.
* Reports the number of warnings encountered during analysis.
*
* @remarks
* This does not count warnings that are emitted in the API report file.
*/
public readonly warningCount: number;

Expand All @@ -102,6 +120,7 @@ export class ExtractorResult {
this.compilerState = properties.compilerState;
this.extractorConfig = properties.extractorConfig;
this.succeeded = properties.succeeded;
this.apiReportChanged = properties.apiReportChanged;
this.errorCount = properties.errorCount;
this.warningCount = properties.warningCount;
}
Expand Down Expand Up @@ -130,13 +149,6 @@ export class Extractor {
return PackageJsonLookup.loadOwnPackageJson(__dirname);
}

private static _defaultLogger: ILogger = {
logVerbose: (message: string) => console.log('(Verbose) ' + message),
logInfo: (message: string) => console.log(message),
logWarning: (message: string) => console.warn(colors.yellow(message)),
logError: (message: string) => console.error(colors.red(message))
};

/**
* Load the api-extractor.json config file from the specified path, and then invoke API Extractor.
*/
Expand All @@ -155,18 +167,8 @@ export class Extractor {
options = { };
}

let mergedLogger: ILogger;
if (options && options.customLogger) {
mergedLogger = lodash.merge(lodash.clone(Extractor._defaultLogger), options.customLogger);
} else {
mergedLogger = Extractor._defaultLogger;
}
const monitoredLogger: MonitoredLogger = new MonitoredLogger(mergedLogger);

const localBuild: boolean = options.localBuild || false;

monitoredLogger.resetCounters();

let compilerState: CompilerState | undefined;
if (options.compilerState) {
compilerState = options.compilerState;
Expand All @@ -176,10 +178,13 @@ export class Extractor {

const collector: Collector = new Collector({
program: compilerState.program,
logger: monitoredLogger,
messageCallback: options.messageCallback,
extractorConfig: extractorConfig
});

const messageRouter: MessageRouter = collector.messageRouter;
messageRouter.showVerboseMessages = !!options.showVerboseMessages;

collector.analyze();

DocCommentEnhancer.analyze(collector);
Expand All @@ -189,7 +194,7 @@ export class Extractor {
const apiPackage: ApiPackage = modelBuilder.buildApiPackage();

if (extractorConfig.docModelEnabled) {
monitoredLogger.logVerbose('Writing: ' + extractorConfig.apiJsonFilePath);
messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, 'Writing: ' + extractorConfig.apiJsonFilePath);
apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, {
toolPackage: Extractor.packageName,
toolVersion: Extractor.version,
Expand All @@ -200,6 +205,8 @@ export class Extractor {
});
}

let apiReportChanged: boolean = false;

if (extractorConfig.apiReportEnabled) {
const actualApiReportPath: string = extractorConfig.reportTempFilePath;
const actualApiReviewShortPath: string = extractorConfig._getShortFilePath(extractorConfig.reportTempFilePath);
Expand All @@ -222,81 +229,77 @@ export class Extractor {
if (!ReviewFileGenerator.areEquivalentApiFileContents(actualApiReviewContent, expectedApiReviewContent)) {
if (!localBuild) {
// For production, issue a warning that will break the CI build.
monitoredLogger.logWarning('You have changed the public API signature for this project.'
messageRouter.logWarning(ConsoleMessageId.ApiReportNotCopied,
'You have changed the public API signature for this project.'
// @microsoft/gulp-core-build seems to run JSON.stringify() on the error messages for some reason,
// so try to avoid escaped characters:
+ ` Please overwrite ${expectedApiReviewShortPath} with a`
+ ` copy of ${actualApiReviewShortPath}`
+ ' and then request an API review. See the Git repository README.md for more info.');
} else {
// For a local build, just copy the file automatically.
monitoredLogger.logWarning('You have changed the public API signature for this project.'
messageRouter.logWarning(ConsoleMessageId.ApiReportCopied,
'You have changed the public API signature for this project.'
+ ` Updating ${expectedApiReviewShortPath}`);

FileSystem.writeFile(expectedApiReviewPath, actualApiReviewContent, {
ensureFolderExists: true,
convertLineEndings: NewlineKind.CrLf
});
}

apiReportChanged = true;
} else {
monitoredLogger.logVerbose(`The API signature is up to date: ${actualApiReviewShortPath}`);
messageRouter.logVerbose(ConsoleMessageId.ApiReportUnchanged,
`The API signature is up to date: ${actualApiReviewShortPath}`);
}
} else {
// NOTE: This warning seems like a nuisance, but it has caught genuine mistakes.
// For example, when projects were moved into category folders, the relative path for
// the API review files ended up in the wrong place.
monitoredLogger.logError(`The API review file has not been set up.`
messageRouter.logError(ConsoleMessageId.ApiReportMissing, `The API review file has not been set up.`
+ ` Do this by copying ${actualApiReviewShortPath}`
+ ` to ${expectedApiReviewShortPath} and committing it.`);
}
}

if (extractorConfig.rollupEnabled) {
Extractor._generateRollupDtsFile(collector, monitoredLogger,
extractorConfig.publicTrimmedFilePath,
DtsRollupKind.PublicRelease);

Extractor._generateRollupDtsFile(collector, monitoredLogger,
extractorConfig.betaTrimmedFilePath,
DtsRollupKind.BetaRelease);

Extractor._generateRollupDtsFile(collector, monitoredLogger,
extractorConfig.untrimmedFilePath,
DtsRollupKind.InternalRelease);
Extractor._generateRollupDtsFile(collector, extractorConfig.publicTrimmedFilePath, DtsRollupKind.PublicRelease);
Extractor._generateRollupDtsFile(collector, extractorConfig.betaTrimmedFilePath, DtsRollupKind.BetaRelease);
Extractor._generateRollupDtsFile(collector, extractorConfig.untrimmedFilePath, DtsRollupKind.InternalRelease);
}

if (extractorConfig.tsdocMetadataEnabled) {
// Write the tsdoc-metadata.json file for this project
PackageMetadataManager.writeTsdocMetadataFile(extractorConfig.tsdocMetadataFilePath);
}

// Show out all the messages that we collected during analysis
collector.messageRouter.reportMessagesToLogger(monitoredLogger, collector.workingPackage.packageFolder);
// Show all the messages that we collected during analysis
messageRouter.handleRemainingNonConsoleMessages();

// Determine success
let succeeded: boolean;
if (localBuild) {
// For a local build, fail if there were errors (but ignore warnings)
succeeded = monitoredLogger.errorCount === 0;
succeeded = messageRouter.errorCount === 0;
} else {
// For a production build, fail if there were any errors or warnings
succeeded = monitoredLogger.errorCount + monitoredLogger.warningCount === 0;
succeeded = messageRouter.errorCount + messageRouter.warningCount === 0;
}

return new ExtractorResult({
compilerState,
extractorConfig,
succeeded,
errorCount: monitoredLogger.errorCount,
warningCount: monitoredLogger.warningCount
apiReportChanged,
errorCount: messageRouter.errorCount,
warningCount: messageRouter.warningCount
});
}

private static _generateRollupDtsFile(collector: Collector, monitoredLogger: MonitoredLogger,
outputPath: string, dtsKind: DtsRollupKind): void {

private static _generateRollupDtsFile(collector: Collector, outputPath: string, dtsKind: DtsRollupKind): void {
if (outputPath !== '') {
monitoredLogger.logVerbose(`Writing package typings: ${outputPath}`);
collector.messageRouter.logVerbose(ConsoleMessageId.WritingDtsRollup, `Writing package typings: ${outputPath}`);
DtsRollupGenerator.writeTypingsFile(collector, outputPath, dtsKind);
}
}
Expand Down
Loading