@@ -44,14 +44,47 @@ export interface IExtractorConfigTokens {
4444}
4545
4646/**
47- * Options for {@link ExtractorConfig.parseConfig }.
47+ * Options for {@link ExtractorConfig.parseConfigObject }.
4848 *
4949 * @public
5050 */
51- export interface IExtractorConfigParseConfigOptions {
52- mergedConfig : Partial < IExtractorConfig > ;
53- mergedConfigFullPath : string ;
54- packageJsonPath : string | undefined ;
51+ export interface IExtractorConfigParseConfigObjectOptions {
52+ /**
53+ * An already prepared configuration object as returned by {@link ExtractorConfig.loadJsonFileWithInheritance}.
54+ */
55+ configObject : Partial < IExtractorConfig > ;
56+
57+ /**
58+ * The absolute path of the file that the `configObject` object was loaded from. This is used for error messages
59+ * and when probing for `tsconfig.json`.
60+ *
61+ * @remarks
62+ *
63+ * If this is omitted, then the `rootFolder` must not be specified using the `<lookup>` token.
64+ */
65+ configObjectFullPath : string | undefined ;
66+
67+ /**
68+ * The parsed package.json file for the working package, or undefined if API Extractor was invoked without
69+ * a package.json file.
70+ *
71+ * @remarks
72+ *
73+ * If omitted, then the `<unscopedPackageName>` and `<packageName>` tokens will have default values.
74+ */
75+ packageJson ?: INodePackageJson | undefined ;
76+
77+ /**
78+ * The absolute path of the file that the `packageJson` object was loaded from, or undefined if API Extractor
79+ * was invoked without a package.json file.
80+ *
81+ * @remarks
82+ *
83+ * This is used for error messages and when resolving paths found in package.json.
84+ *
85+ * If `packageJsonFullPath` is specified but `packageJson` is omitted, the file will be loaded automatically.
86+ */
87+ packageJsonFullPath : string | undefined ;
5588}
5689
5790/**
@@ -79,18 +112,16 @@ export class ExtractorConfig {
79112 public rootFolder : string = '' ;
80113
81114 /**
82- * Returns the folder for the package.json file of the working package.
83- *
84- * @remarks
85- * If the entry point is `C:\Folder\project\src\index.ts` and the nearest package.json
86- * is `C:\Folder\project\package.json`, then the packageFolder is `C:\Folder\project`
115+ * The parsed package.json file for the working package, or undefined if API Extractor was invoked without
116+ * a package.json file.
87117 */
88- public packageFolder : string = '' ;
118+ public packageJson : INodePackageJson | undefined = undefined ;
89119
90120 /**
91- * The parsed package.json file for the working package.
121+ * The absolute path of the file that package.json was loaded from, or undefined if API Extractor was invoked without
122+ * a package.json file.
92123 */
93- public packageJson : INodePackageJson ;
124+ public packageJsonFullPath : string | undefined = undefined ;
94125
95126 /** {@inheritDoc IExtractorConfigTokens } */
96127 public readonly tokens : IExtractorConfigTokens ;
@@ -165,20 +196,25 @@ export class ExtractorConfig {
165196 * along with the API Extractor defaults.
166197 * The result is parsed and returned.
167198 */
168- public static loadAndParseConfig ( jsonFilePath : string ) : ExtractorConfig {
169- const mergedConfig : Partial < IExtractorConfig > = ExtractorConfig . loadJsonFileWithInheritance ( jsonFilePath ) ;
199+ public static loadAndParseConfig ( configJsonFilePath : string ) : ExtractorConfig {
200+ const configObjectFullPath : string = path . resolve ( configJsonFilePath ) ;
201+ const configObject : Partial < IExtractorConfig > = ExtractorConfig . loadJsonFileWithInheritance ( configObjectFullPath ) ;
202+
203+ const packageJsonLookup : PackageJsonLookup = new PackageJsonLookup ( ) ;
204+ const packageJsonFullPath : string | undefined = packageJsonLookup . tryGetPackageJsonFilePathFor (
205+ configObjectFullPath ) ;
170206
171207 const extractorConfig : ExtractorConfig = ExtractorConfig . parseConfigObject ( {
172- mergedConfig ,
173- mergedConfigFullPath : jsonFilePath ,
174- packageJsonPath : undefined
208+ configObject ,
209+ configObjectFullPath ,
210+ packageJsonFullPath
175211 } ) ;
176212
177213 return extractorConfig ;
178214 }
179215
180216 /**
181- * Performs only the first half of {@link ExtractorConfig.loadAndParseFile }, providing an opportunity to
217+ * Performs only the first half of {@link ExtractorConfig.loadAndParseConfig }, providing an opportunity to
182218 * modify the object before it is pssed to {@link ExtractorConfig.parseConfigObject}.
183219 *
184220 * @remarks
@@ -194,10 +230,10 @@ export class ExtractorConfig {
194230 // Get absolute path of config file.
195231 let currentConfigFilePath : string = path . resolve ( process . cwd ( ) , jsonFilePath ) ;
196232
197- let mergedConfig : Partial < IExtractorConfig > = JsonFile . load ( currentConfigFilePath ) ;
233+ let configObject : Partial < IExtractorConfig > = JsonFile . load ( currentConfigFilePath ) ;
198234
199235 try {
200- while ( mergedConfig . extends ) {
236+ while ( configObject . extends ) {
201237 // Check if this file was already processed.
202238 if ( visitedPaths . has ( currentConfigFilePath ) ) {
203239 throw new Error ( `The API Extractor config files contain a cycle. "${ currentConfigFilePath } "`
@@ -207,15 +243,15 @@ export class ExtractorConfig {
207243
208244 const currentConfigFolderPath : string = path . dirname ( currentConfigFilePath ) ;
209245
210- if ( mergedConfig . extends . match ( / ^ \. \. ? [ \\ / ] / ) ) {
246+ if ( configObject . extends . match ( / ^ \. \. ? [ \\ / ] / ) ) {
211247 // EXAMPLE: "./subfolder/api-extractor-base.json"
212- currentConfigFilePath = path . resolve ( currentConfigFolderPath , mergedConfig . extends ) ;
248+ currentConfigFilePath = path . resolve ( currentConfigFolderPath , configObject . extends ) ;
213249 } else {
214250 // EXAMPLE: "my-package/api-extractor-base.json"
215251 //
216252 // Resolve "my-package" from the perspective of the current folder.
217253 currentConfigFilePath = resolve . sync (
218- mergedConfig . extends ,
254+ configObject . extends ,
219255 {
220256 basedir : currentConfigFolderPath
221257 }
@@ -226,53 +262,69 @@ export class ExtractorConfig {
226262 const baseConfig : IExtractorConfig = JsonFile . load ( currentConfigFilePath ) ;
227263
228264 // Delete the "extends" field, since we've already expanded it
229- delete mergedConfig . extends ;
265+ delete configObject . extends ;
230266
231267 // Merge extractorConfig into baseConfig, mutating baseConfig
232- lodash . merge ( baseConfig , mergedConfig ) ;
268+ lodash . merge ( baseConfig , configObject ) ;
233269
234- mergedConfig = baseConfig ;
270+ configObject = baseConfig ;
235271 }
236272 } catch ( e ) {
237273 throw new Error ( `Error loading ${ currentConfigFilePath } :\n` + e . message ) ;
238274 }
239275
240276 // Lastly, apply the defaults
241- mergedConfig = lodash . merge ( lodash . cloneDeep ( ExtractorConfig . _defaultConfig ) , mergedConfig ) ;
277+ configObject = lodash . merge ( lodash . cloneDeep ( ExtractorConfig . _defaultConfig ) , configObject ) ;
242278
243- return mergedConfig ;
279+ return configObject ;
244280 }
245281
246282 /**
247283 * Parses the api-extractor.json configuration provided as a runtime object, rather than reading it from disk.
248284 * This allows configurations to be customized or constructed programmatically.
249285 */
250- public static parseConfigObject ( options : IExtractorConfigParseConfigOptions ) : ExtractorConfig {
251- const mergedConfigFullPath : string = options . mergedConfigFullPath ;
252- const mergedConfig : Partial < IExtractorConfig > = options . mergedConfig ;
286+ public static parseConfigObject ( options : IExtractorConfigParseConfigObjectOptions ) : ExtractorConfig {
287+ const filenameForErrors : string = options . configObjectFullPath || 'the configuration object' ;
288+ const configObject : Partial < IExtractorConfig > = options . configObject ;
253289
254- if ( ! path . isAbsolute ( mergedConfigFullPath ) ) {
255- throw new Error ( 'filenameForErrors must be an absolute path' ) ;
290+ if ( options . configObjectFullPath ) {
291+ if ( ! path . isAbsolute ( options . configObjectFullPath ) ) {
292+ throw new Error ( 'configObjectFullPath must be an absolute path' ) ;
293+ }
256294 }
257295
258- ExtractorConfig . jsonSchema . validateObject ( mergedConfig , mergedConfigFullPath ) ;
296+ ExtractorConfig . jsonSchema . validateObject ( configObject , filenameForErrors ) ;
259297
260298 const result : ExtractorConfig = new ExtractorConfig ( ) ;
261299
300+ if ( options . packageJsonFullPath ) {
301+ if ( path . isAbsolute ( options . packageJsonFullPath ) ) {
302+ throw new Error ( 'packageJsonFullPath must be an absolute path' ) ;
303+ }
304+
305+ if ( ! options . packageJson ) {
306+ const packageJsonLookup : PackageJsonLookup = new PackageJsonLookup ( ) ;
307+ result . packageJson = packageJsonLookup . loadNodePackageJson ( options . packageJsonFullPath ) ;
308+ }
309+ }
310+
262311 try {
263312
264- if ( ! mergedConfig . compiler ) {
313+ if ( ! configObject . compiler ) {
265314 // A merged configuration should have this
266315 throw new Error ( 'The "compiler" section is missing' ) ;
267316 }
268317
269- if ( mergedConfig . compiler . rootFolder . trim ( ) === '<lookup>' ) {
318+ if ( configObject . compiler . rootFolder . trim ( ) === '<lookup>' ) {
319+ if ( ! options . configObjectFullPath ) {
320+ throw new Error ( 'The "<lookup>" token cannot be expanded because configObjectFullPath was not specified' ) ;
321+ }
270322 // "The default value for `rootFolder` is the token `<lookup>`, which means the folder is determined
271323 // by traversing parent folders, starting from the folder containing api-extractor.json, and stopping
272324 // at the first folder that contains a tsconfig.json file. If a tsconfig.json file cannot be found in
273325 // this way, then an error will be reported."
274326
275- let currentFolder : string = path . dirname ( mergedConfigFullPath ) ;
327+ let currentFolder : string = path . dirname ( options . configObjectFullPath ) ;
276328 for ( ; ; ) {
277329 const tsconfigPath : string = path . join ( currentFolder , 'tsconfig.json' ) ;
278330 if ( FileSystem . exists ( tsconfigPath ) ) {
@@ -287,34 +339,29 @@ export class ExtractorConfig {
287339 currentFolder = parentFolder ;
288340 }
289341 } else {
290- if ( ! mergedConfig . compiler . rootFolder ) {
342+ if ( ! configObject . compiler . rootFolder ) {
291343 throw new Error ( 'The rootFolder must be specified' ) ;
292344 }
293345
294- ExtractorConfig . _rejectAnyTokensInPath ( mergedConfig . compiler . rootFolder , 'rootFolder' ) ;
346+ ExtractorConfig . _rejectAnyTokensInPath ( configObject . compiler . rootFolder , 'rootFolder' ) ;
295347
296- if ( ! FileSystem . exists ( mergedConfig . compiler . rootFolder ) ) {
297- throw new Error ( 'The specified rootFolder does not exist: ' + mergedConfig . compiler . rootFolder ) ;
348+ if ( ! FileSystem . exists ( configObject . compiler . rootFolder ) ) {
349+ throw new Error ( 'The specified rootFolder does not exist: ' + configObject . compiler . rootFolder ) ;
298350 }
299351
300- result . rootFolder = mergedConfig . compiler . rootFolder ;
352+ result . rootFolder = configObject . compiler . rootFolder ;
301353 }
302354
303- if ( options . packageJsonPath !== undefined ) {
304- result . packageFolder = path . dirname ( options . packageJsonPath ) ;
305- const packageJsonLookup : PackageJsonLookup = new PackageJsonLookup ( ) ;
306- result . packageJson = packageJsonLookup . loadNodePackageJson ( options . packageJsonPath ) ;
355+ if ( result . packageJson ) {
307356 result . tokens . packageName = result . packageJson . name ;
308357 result . tokens . unscopedPackageName = PackageName . getUnscopedName ( result . packageJson . name ) ;
309- } else {
310- result . packageFolder = result . rootFolder ;
311358 }
312359
313- if ( ! mergedConfig . mainEntryPointFile ) {
360+ if ( ! configObject . mainEntryPointFile ) {
314361 // A merged configuration should have this
315362 throw new Error ( 'mainEntryPointFile is missing' ) ;
316363 }
317- result . mainEntryPointFile = result . _resolvePathWithTokens ( 'mainEntryPointFile' , mergedConfig . mainEntryPointFile ) ;
364+ result . mainEntryPointFile = result . _resolvePathWithTokens ( 'mainEntryPointFile' , configObject . mainEntryPointFile ) ;
318365
319366 if ( ! ExtractorConfig . hasDtsFileExtension ( result . mainEntryPointFile ) ) {
320367 throw new Error ( 'The mainEntryPointFile is not a declaration file: ' + result . mainEntryPointFile ) ;
@@ -324,13 +371,13 @@ export class ExtractorConfig {
324371 throw new Error ( 'The mainEntryPointFile does not exist: ' + result . mainEntryPointFile ) ;
325372 }
326373
327- result . overrideTsconfig = mergedConfig . compiler . overrideTsconfig ;
328- result . skipLibCheck = ! ! mergedConfig . compiler . skipLibCheck ;
374+ result . overrideTsconfig = configObject . compiler . overrideTsconfig ;
375+ result . skipLibCheck = ! ! configObject . compiler . skipLibCheck ;
329376
330- if ( mergedConfig . apiReport ) {
331- result . apiReportEnabled = ! ! mergedConfig . apiReport . enabled ;
377+ if ( configObject . apiReport ) {
378+ result . apiReportEnabled = ! ! configObject . apiReport . enabled ;
332379
333- const reportFilename : string = mergedConfig . apiReport . reportFileName || '' ;
380+ const reportFilename : string = configObject . apiReport . reportFileName || '' ;
334381 if ( ! reportFilename ) {
335382 // A merged configuration should have this
336383 throw new Error ( 'reportFilename is missing' ) ;
@@ -341,42 +388,50 @@ export class ExtractorConfig {
341388 }
342389
343390 const reportFolder : string = result . _resolvePathWithTokens ( 'reportFolder' ,
344- mergedConfig . apiReport . reportFolder ) ;
391+ configObject . apiReport . reportFolder ) ;
345392 const tempFolder : string = result . _resolvePathWithTokens ( 'tempFolder' ,
346- mergedConfig . apiReport . tempFolder ) ;
393+ configObject . apiReport . tempFolder ) ;
347394
348395 result . reportFilePath = path . join ( reportFolder , reportFilename ) ;
349396 result . tempReportFilePath = path . join ( tempFolder , reportFilename ) ;
350397 }
351398
352- if ( mergedConfig . docModel ) {
353- result . apiReportEnabled = ! ! mergedConfig . docModel . enabled ;
399+ if ( configObject . docModel ) {
400+ result . apiReportEnabled = ! ! configObject . docModel . enabled ;
354401 result . apiJsonFilePath = result . _resolvePathWithTokens ( 'apiJsonFilePath' ,
355- mergedConfig . docModel . apiJsonFilePath ) ;
402+ configObject . docModel . apiJsonFilePath ) ;
356403 }
357404
358- if ( mergedConfig . tsdocMetadata ) {
359- result . tsdocMetadataEnabled = ! ! mergedConfig . tsdocMetadata . enabled ;
405+ if ( configObject . tsdocMetadata ) {
406+ result . tsdocMetadataEnabled = ! ! configObject . tsdocMetadata . enabled ;
360407
361- if ( mergedConfig . compiler . rootFolder . trim ( ) === '<lookup>' ) {
408+ if ( configObject . compiler . rootFolder . trim ( ) === '<lookup>' ) {
409+ if ( ! result . packageJson ) {
410+ throw new Error ( 'The "<lookup>" token cannot be used with compiler.rootFolder because'
411+ + 'the "packageJson" option was not provided' ) ;
412+ }
413+ if ( ! result . packageJsonFullPath ) {
414+ throw new Error ( 'The "<lookup>" token cannot be used with compiler.rootFolder because'
415+ + 'the "packageJsonFullPath" option was not provided' ) ;
416+ }
362417 result . tsdocMetadataFilePath = PackageMetadataManager . resolveTsdocMetadataPath (
363- result . packageFolder ,
418+ path . dirname ( result . packageJsonFullPath ) ,
364419 result . packageJson ,
365420 result . tsdocMetadataFilePath
366421 ) ;
367422 } else {
368423 result . tsdocMetadataFilePath = result . _resolvePathWithTokens ( 'tsdocMetadataFilePath' ,
369- mergedConfig . tsdocMetadata . tsdocMetadataFilePath ) ;
424+ configObject . tsdocMetadata . tsdocMetadataFilePath ) ;
370425 }
371426 }
372427
373- if ( mergedConfig . messages ) {
374- result . messages = mergedConfig . messages ;
428+ if ( configObject . messages ) {
429+ result . messages = configObject . messages ;
375430 }
376431
377- result . testMode = ! ! mergedConfig . testMode ;
432+ result . testMode = ! ! configObject . testMode ;
378433 } catch ( e ) {
379- throw new Error ( `Error parsing ${ mergedConfigFullPath } :\n` + e . message ) ;
434+ throw new Error ( `Error parsing ${ filenameForErrors } :\n` + e . message ) ;
380435 }
381436 return result ;
382437 }
0 commit comments