From b1558c5af7363508e97b54d586816bb67eff742a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:00:00 +0200 Subject: [PATCH] docgen: add proper cli entrypoint end improve path handling --- packages/docgen/src/docgen/ApiDocGenerator.ts | 13 +--- packages/docgen/src/docgen/ApiDocPrinter.ts | 4 +- packages/docgen/src/docgen/TypeLocator.ts | 19 ++--- .../docgen/src/{compiler.ts => generate.ts} | 78 ++++++------------- packages/docgen/src/index.ts | 44 ++++++++++- 5 files changed, 83 insertions(+), 75 deletions(-) rename packages/docgen/src/{compiler.ts => generate.ts} (53%) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 01b1e0bf0f..291c3f5b83 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -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, diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index a97885430a..43107a34ec 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -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"}`; } diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index d5e7025f09..164275b13f 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -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( typeLookupTable: { [key in T]: ts.Type }, - roots: string[], ): { [key in T]: ExportedInstance[] } { const docMap = new Map(); for (const type of Object.values(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; } diff --git a/packages/docgen/src/compiler.ts b/packages/docgen/src/generate.ts similarity index 53% rename from packages/docgen/src/compiler.ts rename to packages/docgen/src/generate.ts index ebb370f550..59ac45b3ad 100644 --- a/packages/docgen/src/compiler.ts +++ b/packages/docgen/src/generate.ts @@ -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); -}); diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts index d57c49f9f8..69f4ec31ad 100644 --- a/packages/docgen/src/index.ts +++ b/packages/docgen/src/index.ts @@ -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 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);