@@ -11,7 +11,7 @@ import type { ApiPackage } from '@microsoft/api-extractor-model';
1111import { TSDocConfigFile } from '@microsoft/tsdoc-config' ;
1212import {
1313 FileSystem ,
14- type NewlineKind ,
14+ NewlineKind ,
1515 PackageJsonLookup ,
1616 type IPackageJson ,
1717 type INodePackageJson ,
@@ -91,6 +91,15 @@ export interface IExtractorInvokeOptions {
9191 * the STDERR/STDOUT console.
9292 */
9393 messageCallback ?: ( message : ExtractorMessage ) => void ;
94+
95+ /**
96+ * If true, then any differences between the actual and expected API reports will be
97+ * printed on the console.
98+ *
99+ * @remarks
100+ * The diff is not printed if the expected API report file has not been created yet.
101+ */
102+ printApiReportDiff ?: boolean ;
94103}
95104
96105/**
@@ -192,36 +201,52 @@ export class Extractor {
192201 * Invoke API Extractor using an already prepared `ExtractorConfig` object.
193202 */
194203 public static invoke ( extractorConfig : ExtractorConfig , options ?: IExtractorInvokeOptions ) : ExtractorResult {
195- if ( ! options ) {
196- options = { } ;
197- }
198-
199- const localBuild : boolean = options . localBuild || false ;
200-
201- let compilerState : CompilerState | undefined ;
202- if ( options . compilerState ) {
203- compilerState = options . compilerState ;
204- } else {
205- compilerState = CompilerState . create ( extractorConfig , options ) ;
206- }
204+ const {
205+ packageFolder,
206+ messages,
207+ tsdocConfiguration,
208+ tsdocConfigFile : { filePath : tsdocConfigFilePath , fileNotFound : tsdocConfigFileNotFound } ,
209+ apiJsonFilePath,
210+ newlineKind,
211+ reportTempFolder,
212+ reportFolder,
213+ apiReportEnabled,
214+ reportConfigs,
215+ testMode,
216+ rollupEnabled,
217+ publicTrimmedFilePath,
218+ alphaTrimmedFilePath,
219+ betaTrimmedFilePath,
220+ untrimmedFilePath,
221+ tsdocMetadataEnabled,
222+ tsdocMetadataFilePath
223+ } = extractorConfig ;
224+ const {
225+ localBuild = false ,
226+ compilerState = CompilerState . create ( extractorConfig , options ) ,
227+ messageCallback,
228+ showVerboseMessages = false ,
229+ showDiagnostics = false ,
230+ printApiReportDiff = false
231+ } = options ?? { } ;
207232
208233 const sourceMapper : SourceMapper = new SourceMapper ( ) ;
209234
210235 const messageRouter : MessageRouter = new MessageRouter ( {
211- workingPackageFolder : extractorConfig . packageFolder ,
212- messageCallback : options . messageCallback ,
213- messagesConfig : extractorConfig . messages || { } ,
214- showVerboseMessages : ! ! options . showVerboseMessages ,
215- showDiagnostics : ! ! options . showDiagnostics ,
216- tsdocConfiguration : extractorConfig . tsdocConfiguration ,
236+ workingPackageFolder : packageFolder ,
237+ messageCallback,
238+ messagesConfig : messages || { } ,
239+ showVerboseMessages,
240+ showDiagnostics,
241+ tsdocConfiguration,
217242 sourceMapper
218243 } ) ;
219244
220- if ( extractorConfig . tsdocConfigFile . filePath && ! extractorConfig . tsdocConfigFile . fileNotFound ) {
221- if ( ! Path . isEqual ( extractorConfig . tsdocConfigFile . filePath , ExtractorConfig . _tsdocBaseFilePath ) ) {
245+ if ( tsdocConfigFilePath && ! tsdocConfigFileNotFound ) {
246+ if ( ! Path . isEqual ( tsdocConfigFilePath , ExtractorConfig . _tsdocBaseFilePath ) ) {
222247 messageRouter . logVerbose (
223248 ConsoleMessageId . UsingCustomTSDocConfig ,
224- ' Using custom TSDoc config from ' + extractorConfig . tsdocConfigFile . filePath
249+ ` Using custom TSDoc config from ${ tsdocConfigFilePath } `
225250 ) ;
226251 }
227252 }
@@ -243,9 +268,7 @@ export class Extractor {
243268
244269 messageRouter . logDiagnosticHeader ( 'TSDoc configuration' ) ;
245270 // Convert the TSDocConfiguration into a tsdoc.json representation
246- const combinedConfigFile : TSDocConfigFile = TSDocConfigFile . loadFromParser (
247- extractorConfig . tsdocConfiguration
248- ) ;
271+ const combinedConfigFile : TSDocConfigFile = TSDocConfigFile . loadFromParser ( tsdocConfiguration ) ;
249272 const serializedTSDocConfig : object = MessageRouter . buildJsonDumpObject (
250273 combinedConfigFile . saveToObject ( )
251274 ) ;
@@ -273,17 +296,14 @@ export class Extractor {
273296 }
274297
275298 if ( modelBuilder . docModelEnabled ) {
276- messageRouter . logVerbose (
277- ConsoleMessageId . WritingDocModelFile ,
278- 'Writing: ' + extractorConfig . apiJsonFilePath
279- ) ;
280- apiPackage . saveToJsonFile ( extractorConfig . apiJsonFilePath , {
299+ messageRouter . logVerbose ( ConsoleMessageId . WritingDocModelFile , `Writing: ${ apiJsonFilePath } ` ) ;
300+ apiPackage . saveToJsonFile ( apiJsonFilePath , {
281301 toolPackage : Extractor . packageName ,
282302 toolVersion : Extractor . version ,
283303
284- newlineConversion : extractorConfig . newlineKind ,
304+ newlineConversion : newlineKind ,
285305 ensureFolderExists : true ,
286- testMode : extractorConfig . testMode
306+ testMode
287307 } ) ;
288308 }
289309
@@ -292,53 +312,51 @@ export class Extractor {
292312 collector ,
293313 extractorConfig ,
294314 messageRouter ,
295- extractorConfig . reportTempFolder ,
296- extractorConfig . reportFolder ,
315+ reportTempFolder ,
316+ reportFolder ,
297317 reportConfig ,
298- localBuild
318+ localBuild ,
319+ printApiReportDiff
299320 ) ;
300321 }
301322
302323 let anyReportChanged : boolean = false ;
303- if ( extractorConfig . apiReportEnabled ) {
304- for ( const reportConfig of extractorConfig . reportConfigs ) {
324+ if ( apiReportEnabled ) {
325+ for ( const reportConfig of reportConfigs ) {
305326 anyReportChanged = writeApiReport ( reportConfig ) || anyReportChanged ;
306327 }
307328 }
308329
309- if ( extractorConfig . rollupEnabled ) {
330+ if ( rollupEnabled ) {
310331 Extractor . _generateRollupDtsFile (
311332 collector ,
312- extractorConfig . publicTrimmedFilePath ,
333+ publicTrimmedFilePath ,
313334 DtsRollupKind . PublicRelease ,
314- extractorConfig . newlineKind
335+ newlineKind
315336 ) ;
316337 Extractor . _generateRollupDtsFile (
317338 collector ,
318- extractorConfig . alphaTrimmedFilePath ,
339+ alphaTrimmedFilePath ,
319340 DtsRollupKind . AlphaRelease ,
320- extractorConfig . newlineKind
341+ newlineKind
321342 ) ;
322343 Extractor . _generateRollupDtsFile (
323344 collector ,
324- extractorConfig . betaTrimmedFilePath ,
345+ betaTrimmedFilePath ,
325346 DtsRollupKind . BetaRelease ,
326- extractorConfig . newlineKind
347+ newlineKind
327348 ) ;
328349 Extractor . _generateRollupDtsFile (
329350 collector ,
330- extractorConfig . untrimmedFilePath ,
351+ untrimmedFilePath ,
331352 DtsRollupKind . InternalRelease ,
332- extractorConfig . newlineKind
353+ newlineKind
333354 ) ;
334355 }
335356
336- if ( extractorConfig . tsdocMetadataEnabled ) {
357+ if ( tsdocMetadataEnabled ) {
337358 // Write the tsdoc-metadata.json file for this project
338- PackageMetadataManager . writeTsdocMetadataFile (
339- extractorConfig . tsdocMetadataFilePath ,
340- extractorConfig . newlineKind
341- ) ;
359+ PackageMetadataManager . writeTsdocMetadataFile ( tsdocMetadataFilePath , newlineKind ) ;
342360 }
343361
344362 // Show all the messages that we collected during analysis
@@ -373,6 +391,7 @@ export class Extractor {
373391 * @param reportDirectoryPath - The path to the directory under which the existing report file is located, and to
374392 * which the new report will be written post-comparison.
375393 * @param reportConfig - API report configuration, including its file name and {@link ApiReportVariant}.
394+ * @param printApiReportDiff - {@link IExtractorInvokeOptions.printApiReportDiff}
376395 *
377396 * @returns Whether or not the newly generated report differs from the existing report (if one exists).
378397 */
@@ -383,7 +402,8 @@ export class Extractor {
383402 reportTempDirectoryPath : string ,
384403 reportDirectoryPath : string ,
385404 reportConfig : IExtractorConfigApiReport ,
386- localBuild : boolean
405+ localBuild : boolean ,
406+ printApiReportDiff : boolean
387407 ) : boolean {
388408 let apiReportChanged : boolean = false ;
389409
@@ -411,7 +431,9 @@ export class Extractor {
411431
412432 // Compare it against the expected file
413433 if ( FileSystem . exists ( expectedApiReportPath ) ) {
414- const expectedApiReportContent : string = FileSystem . readFile ( expectedApiReportPath ) ;
434+ const expectedApiReportContent : string = FileSystem . readFile ( expectedApiReportPath , {
435+ convertLineEndings : NewlineKind . Lf
436+ } ) ;
415437
416438 if (
417439 ! ApiReportGenerator . areEquivalentApiFileContents ( actualApiReportContent , expectedApiReportContent )
@@ -439,6 +461,26 @@ export class Extractor {
439461 convertLineEndings : extractorConfig . newlineKind
440462 } ) ;
441463 }
464+
465+ if ( messageRouter . showVerboseMessages || printApiReportDiff ) {
466+ const Diff : typeof import ( 'diff' ) = require ( 'diff' ) ;
467+ const patch : import ( 'diff' ) . StructuredPatch = Diff . structuredPatch (
468+ expectedApiReportShortPath ,
469+ actualApiReportShortPath ,
470+ expectedApiReportContent ,
471+ actualApiReportContent
472+ ) ;
473+ const logFunction :
474+ | ( typeof MessageRouter . prototype ) [ 'logWarning' ]
475+ | ( typeof MessageRouter . prototype ) [ 'logVerbose' ] = printApiReportDiff
476+ ? messageRouter . logWarning . bind ( messageRouter )
477+ : messageRouter . logVerbose . bind ( messageRouter ) ;
478+
479+ logFunction (
480+ ConsoleMessageId . ApiReportDiff ,
481+ 'Changes to the API report:\n\n' + Diff . formatPatch ( patch )
482+ ) ;
483+ }
442484 } else {
443485 messageRouter . logVerbose (
444486 ConsoleMessageId . ApiReportUnchanged ,
0 commit comments