Skip to content

Commit 379e37d

Browse files
committed
Make the package.json file optional for the analyzed project
1 parent 458d838 commit 379e37d

4 files changed

Lines changed: 138 additions & 85 deletions

File tree

apps/api-extractor/src/api/ExtractorConfig.ts

Lines changed: 125 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

apps/api-extractor/src/cli/RunAction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,9 @@ export class RunAction extends CommandLineAction {
125125
const mergedConfig: Partial<IExtractorConfig> = ExtractorConfig.loadJsonFileWithInheritance(configFilename);
126126

127127
const extractorConfig: ExtractorConfig = ExtractorConfig.parseConfigObject({
128-
mergedConfig,
129-
mergedConfigFullPath: configFilename,
130-
packageJsonPath: undefined
128+
configObject: mergedConfig,
129+
configObjectFullPath: configFilename,
130+
packageJsonFullPath: lookup.tryGetPackageFolderFor(configFilename)
131131
});
132132

133133
const extractorResult: ExtractorResult = Extractor.invokeUsingConfig(extractorConfig,

apps/api-extractor/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export {
1919

2020
export {
2121
IExtractorConfigTokens,
22-
IExtractorConfigParseConfigOptions,
22+
IExtractorConfigParseConfigObjectOptions,
2323
ExtractorConfig
2424
} from './api/ExtractorConfig';
2525

0 commit comments

Comments
 (0)