@loopback/tsdocs
Version:
A package to generate api docs using Microsoft api-extractor and api-documenter
281 lines (259 loc) • 7.94 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/tsdocs
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {
CompilerState,
ConsoleMessageId,
Extractor,
ExtractorConfig,
ExtractorLogLevel,
ExtractorMessage,
ExtractorMessageId,
ExtractorResult,
IConfigFile,
} from '@microsoft/api-extractor';
import debugFactory from 'debug';
import fs from 'fs-extra';
import path from 'path';
import {
DEFAULT_APIDOCS_EXTRACTION_PATH,
ExtractorOptions,
LernaPackage,
getPackagesWithTsDocs,
typeScriptPath,
} from './helper';
const debug = debugFactory('loopback:tsdocs');
/**
* Run api-extractor for a lerna-managed monrepo
*
* @remarks
* The function performs the following steps:
* 1. Discover packages with tsdocs from the monorepo
* 2. Iterate through each package to run `api-extractor`
*
* @param options - Options for running api-extractor
*/
export async function runExtractorForMonorepo(options: ExtractorOptions = {}) {
debug('Extractor options:', options);
options = Object.assign(
{
rootDir: process.cwd(),
apiDocsExtractionPath: DEFAULT_APIDOCS_EXTRACTION_PATH,
typescriptCompilerFolder: typeScriptPath,
tsconfigFilePath: 'tsconfig.json',
mainEntryPointFilePath: 'dist/index.d.ts',
},
options,
);
const packages = await getPackagesWithTsDocs(options.rootDir);
/* istanbul ignore if */
if (!packages.length) return;
const lernaRootDir = packages[0].rootPath;
/* istanbul ignore if */
if (!options.silent) {
console.log('Running api-extractor for lerna repo: %s', lernaRootDir);
}
setupApiDocsDirs(lernaRootDir, options);
const errors: Record<string, unknown> = {};
for (const pkg of packages) {
// TODO: api-extractor failed to generate apidocs for the repos below.
// Excluding them for now
// https://github.com/loopbackio/loopback-next/issues/10205
if (
pkg.name === '@loopback/typeorm' ||
pkg.name === '@loopback/boot' ||
pkg.name === '@loopback/express' ||
pkg.name === '@loopback/repository' ||
pkg.name === '@loopback/service-proxy'
)
continue;
/* istanbul ignore if */
const err = invokeExtractorForPackage(pkg, options);
if (err != null) {
if (options.ignoreErrors) {
errors[pkg.name] = err;
} else {
throw err;
}
}
}
if (Object.keys(errors).length === 0) return;
console.error(
'****************************************' +
'****************************************',
);
for (const p in errors) {
const err = errors[p] as {message: string};
console.error('%s: %s', p, err?.message ?? err);
}
console.error(
'****************************************' +
'****************************************',
);
}
export function runExtractorForPackage(
pkgDir: string = process.cwd(),
options: ExtractorOptions = {},
) {
options = Object.assign(
{
rootDir: pkgDir,
apiDocsExtractionPath: DEFAULT_APIDOCS_EXTRACTION_PATH,
typescriptCompilerFolder: typeScriptPath,
tsconfigFilePath: 'tsconfig.json',
mainEntryPointFilePath: 'dist/index.d.ts',
},
options,
);
const pkgJson = require(path.join(pkgDir, 'package.json'));
setupApiDocsDirs(pkgDir, options);
const pkg: LernaPackage = {
private: pkgJson.private,
name: pkgJson.name,
location: pkgDir,
manifestLocation: path.join(pkgDir, 'package.json'),
rootPath: pkgDir,
};
const err = invokeExtractorForPackage(pkg, options);
if (err == null) return;
if (!options.ignoreErrors) {
throw err;
}
console.error(err);
}
/**
* Run `api-extractor` on a given package
* @param pkg - Package descriptor
* @param options - Options for api extraction
*/
function invokeExtractorForPackage(
pkg: LernaPackage,
options: ExtractorOptions,
) {
if (!options.silent) {
console.log('> %s', pkg.name);
}
debug('Package: %s (%s)', pkg.name, pkg.location);
process.chdir(pkg.location);
const extractorConfig = buildExtractorConfig(pkg, options);
debug('Resolved extractor config:', extractorConfig);
try {
invokeExtractor(extractorConfig, options);
} catch (err) {
debug('Error in extracting API docs for %s', pkg.name, err);
return err;
}
}
/**
* Set up dirs for apidocs
*
* @param lernaRootDir - Root dir of the monorepo
* @param options - Extractor options
*/
function setupApiDocsDirs(lernaRootDir: string, options: ExtractorOptions) {
/* istanbul ignore if */
if (options.dryRun) return;
const apiDocsExtractionPath = options.apiDocsExtractionPath!;
fs.emptyDirSync(path.join(lernaRootDir, `${apiDocsExtractionPath}/models`));
if (!options.apiReportEnabled) return;
fs.ensureDirSync(path.join(lernaRootDir, `${apiDocsExtractionPath}/reports`));
fs.emptyDirSync(
path.join(lernaRootDir, `${apiDocsExtractionPath}/reports-temp`),
);
}
/**
* Build extractor configuration object for the given package
*
* @param pkg - Lerna managed package
* @param options - Extractor options
*/
function createRawExtractorConfig(
pkg: LernaPackage,
options: ExtractorOptions,
) {
const entryPoint = path.join(pkg.location, options.mainEntryPointFilePath!);
const apiDocsExtractionPath = options.apiDocsExtractionPath!;
let configObj: IConfigFile = {
projectFolder: pkg.location,
mainEntryPointFilePath: entryPoint,
apiReport: {
enabled: !!options.apiReportEnabled,
reportFolder: path.join(pkg.rootPath, `${apiDocsExtractionPath}/reports`),
reportTempFolder: path.join(
pkg.rootPath,
`${apiDocsExtractionPath}/reports-temp`,
),
reportFileName: '<unscopedPackageName>.api.md',
},
docModel: {
enabled: true,
apiJsonFilePath: path.join(
pkg.rootPath,
`${apiDocsExtractionPath}/models/<unscopedPackageName>.api.json`,
),
},
messages: {
extractorMessageReporting: {
[ExtractorMessageId.MissingReleaseTag]: {
logLevel: ExtractorLogLevel.None,
addToApiReportFile: false,
},
},
},
compiler: {
tsconfigFilePath: options.tsconfigFilePath!,
},
};
/* istanbul ignore if */
if (options.config) {
configObj = Object.assign(configObj, options.config);
}
debug('Extractor config options:', configObj);
return configObj;
}
/**
* Create and prepare the extractor config for invocation
*
* @param pkg - Lerna package
* @param options - Extractor options
*/
function buildExtractorConfig(pkg: LernaPackage, options: ExtractorOptions) {
const configObj: IConfigFile = createRawExtractorConfig(pkg, options);
const extractorConfig = ExtractorConfig.prepare({
configObject: configObj,
configObjectFullPath: '',
packageJsonFullPath: pkg.manifestLocation,
});
return extractorConfig;
}
/**
* Invoke the extractor
*
* @param extractorConfig - Resolved config
* @param options - Extractor options
*/
function invokeExtractor(
extractorConfig: ExtractorConfig,
options: ExtractorOptions,
) {
const compilerState = CompilerState.create(extractorConfig, {
// typescriptCompilerFolder: options.typescriptCompilerFolder,
});
/* istanbul ignore if */
if (options.dryRun) return;
const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
typescriptCompilerFolder: options.typescriptCompilerFolder,
localBuild: true,
showVerboseMessages: !options.silent,
messageCallback: (message: ExtractorMessage) => {
if (message.messageId === ConsoleMessageId.ApiReportCreated) {
// This script deletes the outputs for a clean build,
// so don't issue a warning if the file gets created
message.logLevel = ExtractorLogLevel.None;
}
},
compilerState,
});
debug(extractorResult);
}