docgen: add proper cli entrypoint end improve path handling
This commit is contained in:
@@ -36,17 +36,12 @@ import {
|
||||
* While traversing, it collects information such as names, location in source, docs, etc.
|
||||
*/
|
||||
export default class ApiDocGenerator {
|
||||
static fromProgram(
|
||||
program: ts.Program,
|
||||
basePath: string,
|
||||
sourcePath: string,
|
||||
) {
|
||||
return new ApiDocGenerator(program.getTypeChecker(), basePath, sourcePath);
|
||||
static fromProgram(program: ts.Program, sourcePath: string) {
|
||||
return new ApiDocGenerator(program.getTypeChecker(), sourcePath);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly checker: ts.TypeChecker,
|
||||
private readonly basePath: string,
|
||||
private readonly sourcePath: string,
|
||||
) {}
|
||||
|
||||
@@ -96,7 +91,7 @@ export default class ApiDocGenerator {
|
||||
const name = (type.aliasSymbol || type.symbol).name;
|
||||
const [declaration] = (type.aliasSymbol || type.symbol).declarations;
|
||||
const sourceFile = declaration.getSourceFile();
|
||||
const file = relative(this.basePath, sourceFile.fileName);
|
||||
const file = relative(this.sourcePath, sourceFile.fileName);
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
||||
declaration.getStart(),
|
||||
);
|
||||
@@ -210,7 +205,7 @@ export default class ApiDocGenerator {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
||||
declaration.getStart(),
|
||||
);
|
||||
const file = relative(this.basePath, sourceFile.fileName);
|
||||
const file = relative(this.sourcePath, sourceFile.fileName);
|
||||
const typeInfo = {
|
||||
id: (symbol as any).id,
|
||||
name: symbol.name,
|
||||
|
||||
@@ -19,7 +19,9 @@ import MarkdownPrinter from './MarkdownPrinter';
|
||||
import sortSelector from './sortSelector';
|
||||
import { ApiDoc, InterfaceInfo } from './types';
|
||||
|
||||
// TODO(Rugvip): provide through options?
|
||||
const GH_BASE_URL = 'https://github.com/spotify/backstage';
|
||||
const SRC_PATH = 'packages/core-api/src';
|
||||
|
||||
const COMMIT_SHA =
|
||||
process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8');
|
||||
@@ -37,7 +39,7 @@ export default class ApiDocPrinter {
|
||||
|
||||
mkTypeLink({ file, lineInFile }: { file: string; lineInFile: number }) {
|
||||
const text = `${file}:${lineInFile}`;
|
||||
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
|
||||
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${SRC_PATH}/${file}#L${lineInFile}`;
|
||||
return `[${text}](${href}){:target="_blank"}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,17 +24,15 @@ import { ExportedInstance } from './types';
|
||||
* This is used to e.g. find exported APIs that we should generate documentation for.
|
||||
*/
|
||||
export default class TypeLocator {
|
||||
private readonly checker: ts.TypeChecker;
|
||||
private readonly program: ts.Program;
|
||||
|
||||
static fromProgram(program: ts.Program) {
|
||||
return new TypeLocator(program.getTypeChecker(), program);
|
||||
static fromProgram(program: ts.Program, sourcePath: string) {
|
||||
return new TypeLocator(program.getTypeChecker(), program, sourcePath);
|
||||
}
|
||||
|
||||
constructor(checker: ts.TypeChecker, program: ts.Program) {
|
||||
this.checker = checker;
|
||||
this.program = program;
|
||||
}
|
||||
constructor(
|
||||
private readonly checker: ts.TypeChecker,
|
||||
private readonly program: ts.Program,
|
||||
private readonly sourcePath: string,
|
||||
) {}
|
||||
|
||||
getExportedType(path: string, exportedName: string = 'default'): ts.Type {
|
||||
const source = this.program.getSourceFile(path);
|
||||
@@ -54,7 +52,6 @@ export default class TypeLocator {
|
||||
|
||||
findExportedInstances<T extends string>(
|
||||
typeLookupTable: { [key in T]: ts.Type },
|
||||
roots: string[],
|
||||
): { [key in T]: ExportedInstance[] } {
|
||||
const docMap = new Map<ts.Type, ExportedInstance[]>();
|
||||
for (const type of Object.values<ts.Type>(typeLookupTable)) {
|
||||
@@ -62,7 +59,7 @@ export default class TypeLocator {
|
||||
}
|
||||
|
||||
this.program.getSourceFiles().forEach(source => {
|
||||
const inRoot = roots.some(root => source.fileName.startsWith(root));
|
||||
const inRoot = source.fileName.startsWith(this.sourcePath);
|
||||
if (!inRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import { resolve, join, dirname } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, join as joinPath } from 'path';
|
||||
import ApiDocGenerator from './docgen/ApiDocGenerator';
|
||||
import sortSelector from './docgen/sortSelector';
|
||||
import TypeLocator from './docgen/TypeLocator';
|
||||
@@ -25,49 +24,29 @@ import ApiDocPrinter from './docgen/ApiDocPrinter';
|
||||
import TypescriptHighlighter from './docgen/TypescriptHighlighter';
|
||||
import MarkdownPrinter from './docgen/MarkdownPrinter';
|
||||
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
export async function generate(targetPath: string) {
|
||||
const rootDir = resolvePath(__dirname, '..');
|
||||
const srcDir = resolvePath(rootDir, '..', 'core-api', 'src');
|
||||
const targetDir = resolvePath(targetPath);
|
||||
const docsDir = resolvePath(targetDir, 'docs');
|
||||
|
||||
function loadOptions(path: string): ts.CompilerOptions {
|
||||
const config: any = require(path);
|
||||
let parent = config.extends as string | undefined;
|
||||
|
||||
if (!parent) {
|
||||
return config.compilerOptions;
|
||||
}
|
||||
if (parent.startsWith('.')) {
|
||||
parent = join(dirname(path), parent);
|
||||
}
|
||||
|
||||
return { ...loadOptions(parent), ...config.compilerOptions };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rootDir = resolve(__dirname, '..');
|
||||
const srcDir = resolve(rootDir, '..', 'core-api', 'src');
|
||||
const entrypoint = resolve(srcDir, 'index.ts');
|
||||
const apiRefsDir = resolve(rootDir, 'dist');
|
||||
const mkdocsYaml = resolve(apiRefsDir, 'mkdocs.yml');
|
||||
|
||||
process.chdir(rootDir);
|
||||
|
||||
const options = loadOptions('../../../tsconfig.json');
|
||||
const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json'));
|
||||
|
||||
delete options.moduleResolution;
|
||||
options.removeComments = false;
|
||||
options.noEmit = true;
|
||||
|
||||
const program = ts.createProgram([entrypoint], options);
|
||||
const program = ts.createProgram([resolvePath(srcDir, 'index.ts')], options);
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program);
|
||||
const typeLocator = TypeLocator.fromProgram(program, srcDir);
|
||||
|
||||
const { apis } = typeLocator.findExportedInstances(
|
||||
{
|
||||
apis: typeLocator.getExportedType(entrypoint, 'createApiRef'),
|
||||
},
|
||||
[srcDir],
|
||||
);
|
||||
const { apis } = typeLocator.findExportedInstances({
|
||||
apis: typeLocator.getExportedType(
|
||||
resolvePath(srcDir, 'index.ts'),
|
||||
'createApiRef',
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir, srcDir);
|
||||
const apiDocGenerator = ApiDocGenerator.fromProgram(program, srcDir);
|
||||
const apiDocs = apis
|
||||
.map(api => {
|
||||
try {
|
||||
@@ -90,23 +69,21 @@ async function main() {
|
||||
() => new MarkdownPrinter(new TypescriptHighlighter()),
|
||||
);
|
||||
|
||||
fs.ensureDirSync(resolve(apiRefsDir, 'docs'));
|
||||
fs.ensureDirSync(docsDir);
|
||||
|
||||
await writeFile(
|
||||
join(apiRefsDir, 'docs', 'README.md'),
|
||||
await fs.writeFile(
|
||||
joinPath(docsDir, 'README.md'),
|
||||
apiDocPrinter.printApiIndex(apiDocs),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.values(apiTypes).map(apiType => {
|
||||
const data = apiDocPrinter.printInterface(apiType, apiDocs);
|
||||
for (const apiType of Object.values(apiTypes)) {
|
||||
const data = apiDocPrinter.printInterface(apiType, apiDocs);
|
||||
|
||||
return writeFile(join(apiRefsDir, 'docs', `${apiType.name}.md`), data);
|
||||
}),
|
||||
);
|
||||
await fs.writeFile(joinPath(docsDir, `${apiType.name}.md`), data);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
mkdocsYaml,
|
||||
await fs.writeFile(
|
||||
resolvePath(targetDir, 'mkdocs.yml'),
|
||||
[
|
||||
'site_name: api-references',
|
||||
'nav:',
|
||||
@@ -118,8 +95,3 @@ async function main() {
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -14,4 +14,46 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './compiler';
|
||||
import program from 'commander';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { generate } from './generate';
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
const pkgJson = fs.readJsonSync(resolvePath(__dirname, '../package.json'));
|
||||
program.name('docgen').version(pkgJson.version);
|
||||
|
||||
program
|
||||
.command('generate')
|
||||
.description(
|
||||
'Generate documentation for the declarations in the core-api package',
|
||||
)
|
||||
.option('--output <output>', 'Output directory [./dist]')
|
||||
.action(async cmd => {
|
||||
await generate(cmd.output ?? './dist');
|
||||
});
|
||||
|
||||
program.on('command:*', () => {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red(`Invalid command: ${chalk.cyan(program.args.join(' '))}`),
|
||||
);
|
||||
console.log(chalk.red('See --help for a list of available commands.'));
|
||||
console.log();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp(chalk.yellow);
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', rejection => {
|
||||
console.error(String(rejection));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main(process.argv);
|
||||
|
||||
Reference in New Issue
Block a user