diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index dd9224fc84..5c9d46ea8a 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -68,7 +68,6 @@ devs discoverability Discoverability dls -docgen Dockerfile dockerfiles Dockerize diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 1d5d58c2c9..bf2cc93ea5 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -147,11 +147,6 @@ are separated out into their own folder, see further down. Helps you setup a plugin for isolated development so that it can be served separately. -- [`docgen/`](https://github.com/backstage/backstage/tree/master/packages/docgen) - - Uses the - [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) - to read out definitions and generate documentation for it. - - [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) - Another CLI that can be run to try out what would happen if you build all the packages, publish them, create a new app, and then run them. CI uses this for diff --git a/docs/overview/stability-index.md b/docs/overview/stability-index.md index ee2628a893..8e710d4e7c 100644 --- a/docs/overview/stability-index.md +++ b/docs/overview/stability-index.md @@ -145,12 +145,6 @@ Provides utilities for developing plugins in isolation. Stability: `0`. This package is largely broken and needs updates. -### `docgen` [GitHub](https://github.com/backstage/backstage/tree/master/packages/docgen/) - -Internal CLI utility for generating API Documentation. - -Stability: `N/A` - ### `e2e-test` [GitHub](https://github.com/backstage/backstage/tree/master/packages/e2e-test/) Internal CLI utility for running e2e tests. diff --git a/package.json b/package.json index ea930b060f..d362338370 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "lint:docs": "node ./scripts/check-docs-quality", "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", - "docgen": "lerna run docgen", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", diff --git a/packages/docgen/.eslintrc.js b/packages/docgen/.eslintrc.js deleted file mode 100644 index 503c048748..0000000000 --- a/packages/docgen/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], - rules: { - 'no-console': 0, - }, -}; diff --git a/packages/docgen/README.md b/packages/docgen/README.md deleted file mode 100644 index be1d7cd521..0000000000 --- a/packages/docgen/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# DocGen - API Reference Documentation Generator - -The docgen package provides a CLI to generate markdown documentation for all exported ApiRefs in `@backstage/core`. The documentation is generated based on exported `ApiRef` instances and their type parameters. - -The CLI supports generating both TechDocs and GitHub Markdown, where the TechDocs one provides some better linking and syntax highlighting. - -## Usage - -To generate markdown documentation in the top-level `docs/` directory, run the following: - -```bash -yarn docgen -``` - -## TODO - -This package was lifted out from the Spotify internal Backstage project and could use some further work: - -- Use a higher-level TypeScript compiler library, e.g. `ts-morph`. -- Support for generating docs for any package or multiple packages. -- Better handling of self-referencing types in APIs, e.g. ConfigApi. diff --git a/packages/docgen/bin/backstage-docgen b/packages/docgen/bin/backstage-docgen deleted file mode 100755 index 71674eedd4..0000000000 --- a/packages/docgen/bin/backstage-docgen +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable no-restricted-syntax */ -const path = require('path'); - -// Figure out whether we're running inside the backstage repo or as an installed dependency -const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src')); - -if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { - require('..'); -} else { - require('ts-node').register({ - transpileOnly: true, - project: path.resolve(__dirname, '../../../tsconfig.json'), - compilerOptions: { - module: 'CommonJS', - }, - }); - - require('../src'); -} diff --git a/packages/docgen/package.json b/packages/docgen/package.json deleted file mode 100644 index ad47991159..0000000000 --- a/packages/docgen/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "docgen", - "description": "Tool for generating API Documentation for itself", - "version": "0.1.1", - "private": true, - "homepage": "https://backstage.io", - "repository": { - "type": "git", - "url": "https://github.com/backstage/backstage", - "directory": "packages/docgen" - }, - "keywords": [ - "backstage" - ], - "license": "Apache-2.0", - "main": "src/index.ts", - "scripts": { - "build": "backstage-cli build --outputs cjs", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github && prettier --write ../../docs/reference/utility-apis", - "clean": "backstage-cli clean", - "start": "nodemon --" - }, - "bin": { - "backstage-docgen": "bin/backstage-docgen" - }, - "dependencies": { - "chalk": "^4.0.0", - "commander": "^6.1.0", - "fs-extra": "9.1.0", - "github-slugger": "^1.3.0", - "ts-node": "^10.0.0", - "typescript": "^4.0.3" - }, - "devDependencies": { - "@types/fs-extra": "^9.0.1", - "@types/github-slugger": "^1.3.0", - "@types/node": "^14.14.32", - "nodemon": "^2.0.2" - }, - "files": [ - "bin", - "dist" - ], - "nodemonConfig": { - "watch": "./src", - "exec": "bin/backstage-docgen", - "ext": "ts" - } -} diff --git a/packages/docgen/src/docgen/ApiDocGenerator.test.ts b/packages/docgen/src/docgen/ApiDocGenerator.test.ts deleted file mode 100644 index ea053b1ef4..0000000000 --- a/packages/docgen/src/docgen/ApiDocGenerator.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; -import TypeLocator from './TypeLocator'; -import { createMemProgram } from './testUtils'; -import ApiDocGenerator from './ApiDocGenerator'; - -describe('ApiDocGenerator', () => { - it('should generate empty API doc', () => { - const program = createMemProgram( - ` - import MyApi from './type'; - - type MyApiType = {}; - - export const myApi = new MyApi({ - id: 'my-id', - description: 'my-description', - }); - `, - { - '/mem/type.ts': `export default class MyApi { - constructor(private readonly info: { id: string, description: string }) {} - }`, - }, - ); - - const typeLocator = TypeLocator.fromProgram(program, '/'); - - const { apiInstances } = typeLocator.findExportedInstances({ - apiInstances: typeLocator.getExportedType('/mem/type.ts'), - }); - - expect(apiInstances.length).toBe(1); - const [apiInstance] = apiInstances; - - const docGenerator = ApiDocGenerator.fromProgram(program, '/'); - const doc = docGenerator.toDoc(apiInstance); - - expect(doc.id).toBe('my-id'); - expect(doc.description).toBe('my-description'); - expect(doc.name).toBe('myApi'); - expect(doc.file).toBe('mem/index.ts'); - expect(doc.lineInFile).toBe(6); - expect(doc.interfaceInfos).toEqual([ - { - dependentTypes: [], - docs: [], - file: 'mem/index.ts', - lineInFile: 4, - members: [], - name: 'MyApiType', - }, - ]); - }); - - it('should generate API docs', () => { - const program = createMemProgram( - ` - import MyApi from './type'; - - /** MySubSubType Docs */ - type MySubSubType = { n: number; }; - - /** MySubType Docs */ - type MySubType = { - /** Field a docs */ - a: boolean; - // Field b docs - b: MySubSubType; - } - - /** MySecondSubType Docs */ - - /** With multiple comments */ - export type MySecondSubType = { s: string }; - - // MyThirdSubType Docs that shouldn't show up - type MyThirdSubType = { b: boolean }; - - /** MyApiType Docs */ - type MyApiType = { - /** Docs for x */ - x: string; - // Line comments shouldn't show up - y: MySubType; - /** Multiple */ - /** JsDoc */ - /** Comments */ - z(a: Promise): Array; - }; - - /** Should not show up */ - export const myApi = new MyApi({ - id: 'my-id', - description: 'my-description', - }); - `, - { - '/mem/type.ts': `export default class MyApi { - constructor(private readonly info: { id: string, description: string }) {} - }`, - }, - ); - - const source = program.getSourceFile('/mem/index.ts'); - - // Figure out type IDs so we can make sure they match later - const checker = program.getTypeChecker(); - const symbols = checker.getSymbolsInScope( - source!.getChildren().slice(-1)[0], - ts.SymbolFlags.TypeAlias, - ); - const Ids = [ - 'MySubType', - 'MySubSubType', - 'MySecondSubType', - 'MyThirdSubType', - ].reduce((ids, name) => { - const symbol = symbols.find(s => s.escapedName === name)!; - const type = checker.getTypeAtLocation(symbol.declarations[0]); - ids[name] = (type.aliasSymbol as any).id; - return ids; - }, {} as { [key in string]: number }); - - const typeLocator = TypeLocator.fromProgram(program, '/mem'); - - const { apiInstances } = typeLocator.findExportedInstances({ - apiInstances: typeLocator.getExportedType('/mem/type.ts'), - }); - - expect(apiInstances.length).toBe(1); - const [apiInstance] = apiInstances; - - const docGenerator = ApiDocGenerator.fromProgram(program, '/mem'); - const doc = docGenerator.toDoc(apiInstance); - - expect(doc.id).toBe('my-id'); - expect(doc.description).toBe('my-description'); - expect(doc.name).toBe('myApi'); - expect(doc.file).toBe('index.ts'); - expect(doc.interfaceInfos.length).toBe(1); - expect(doc.interfaceInfos[0].docs).toEqual(['MyApiType Docs']); - expect(doc.interfaceInfos[0].file).toBe('index.ts'); - expect(doc.interfaceInfos[0].lineInFile).toBe(24); - expect(doc.interfaceInfos[0].name).toBe('MyApiType'); - expect(doc.interfaceInfos[0].members).toEqual([ - { - type: 'prop', - name: 'x', - path: 'MyApiType.x', - text: 'x: string', - docs: ['Docs for x'], - links: [], - }, - { - type: 'prop', - name: 'y', - path: 'MyApiType.y', - text: 'y: MySubType', - docs: [], - links: [ - { - id: Ids.MySubType, - name: 'MySubType', - path: 'index.ts/MySubType', - location: [3, 12], - }, - ], - }, - { - type: 'method', - name: 'z', - path: 'MyApiType.z', - text: 'z(a: Promise): Array', - docs: ['Multiple', 'JsDoc', 'Comments'], - links: [ - { - id: Ids.MySecondSubType, - name: 'MySecondSubType', - path: 'index.ts/MySecondSubType', - location: [27, 42], - }, - { - id: Ids.MyThirdSubType, - name: 'MyThirdSubType', - path: 'index.ts/MyThirdSubType', - location: [56, 70], - }, - ], - }, - ]); - expect(doc.interfaceInfos[0].dependentTypes).toEqual([ - { - id: Ids.MySubType, - name: 'MySubType', - path: 'index.ts/MySubType', - file: 'index.ts', - lineInFile: 8, - text: `type MySubType = { - /** Field a docs */ - a: boolean; - // Field b docs - b: MySubSubType; - }`, - docs: ['MySubType Docs'], - links: [ - { - id: Ids.MySubSubType, - name: 'MySubSubType', - path: 'index.ts/MySubSubType', - location: [102, 114], - }, - ], - children: [], - }, - { - id: Ids.MySubSubType, - name: 'MySubSubType', - path: 'index.ts/MySubSubType', - file: 'index.ts', - lineInFile: 5, - text: 'type MySubSubType = { n: number; }', - docs: ['MySubSubType Docs'], - links: [], - children: [], - }, - { - id: Ids.MySecondSubType, - name: 'MySecondSubType', - path: 'index.ts/MySecondSubType', - file: 'index.ts', - lineInFile: 18, - text: 'export type MySecondSubType = { s: string }', - docs: ['MySecondSubType Docs', 'With multiple comments'], - links: [], - children: [], - }, - { - id: Ids.MyThirdSubType, - name: 'MyThirdSubType', - path: 'index.ts/MyThirdSubType', - file: 'index.ts', - lineInFile: 21, - text: 'type MyThirdSubType = { b: boolean }', - docs: [], - links: [], - children: [], - }, - ]); - }); -}); diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts deleted file mode 100644 index 32216745a2..0000000000 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; -import { - relative as relativePath, - sep as pathSep, - posix as posixPath, -} from 'path'; -import { - ExportedInstance, - ApiDoc, - InterfaceInfo, - FieldInfo, - TypeInfo, - TypeLink, -} from './types'; - -// Always use unix path separators for the relative file -// paths, since we'll be using those for HTTP URLs. -function relativeLink(basePath: string, filePath: string) { - return relativePath(basePath, filePath).split(pathSep).join(posixPath.sep); -} - -/** - * The ApiDocGenerator uses the typescript compiler API to build the data structure that - * describes a Backstage API and all of it's related types. - * - * It receives an exported instance that of the form - * `export name = createApiRef({id: ..., description: ...})`. - * It will then traverse all fields and methods on the interface type, and also - * types and declaration of all types that are used by those, both directly and indirectly. - * While traversing, it collects information such as names, location in source, docs, etc. - */ -export default class ApiDocGenerator { - static fromProgram(program: ts.Program, sourcePath: string) { - return new ApiDocGenerator(program.getTypeChecker(), sourcePath); - } - - constructor( - private readonly checker: ts.TypeChecker, - private readonly basePath: string, - ) {} - - /** - * Generate documentation information for a given exported symbol. - */ - toDoc(apiInstance: ExportedInstance): ApiDoc { - const { name, source, args, typeArgs } = apiInstance; - - const [info] = args; - if (!ts.isObjectLiteralExpression(info)) { - throw new Error('api info is not an object literal'); - } - - const id = this.getObjectPropertyLiteral(info, 'id'); - const description = this.getObjectPropertyLiteral(info, 'description'); - const file = relativeLink(this.basePath, source.fileName); - const { line } = source.getLineAndCharacterOfPosition( - apiInstance.node.getStart(), - ); - - const rootTypeNode = typeArgs[0]; - const typeNodes = ts.isIntersectionTypeNode(rootTypeNode) - ? rootTypeNode.types.slice() - : [rootTypeNode]; - const interfaceInfos = typeNodes.map(typeNode => - this.getInterfaceInfo(typeNode), - ); - - return { - id, - name, - file, - lineInFile: line + 1, - description, - interfaceInfos, - }; - } - - /** - * Grab jsDoc and regular comments for a node - */ - private getNodeDocs(node: ts.Node): string[] { - const docNodes = ((node && (node as any).jsDoc) || []) as ts.JSDoc[]; - const docs = docNodes.map(docNode => docNode.comment || '').filter(Boolean); - return docs; - } - - /** - * Collect information about a top-level type. - */ - private getInterfaceInfo(typeNode: ts.TypeNode): InterfaceInfo { - if (ts.isTypeQueryNode(typeNode)) { - throw new Error( - 'APIs must have a proper type parameter, TypeQueries are now allowed', - ); - } - if (!ts.isTypeReferenceNode(typeNode)) { - throw new Error('Interface is not a type node'); - } - - const type = this.checker.getTypeFromTypeNode(typeNode); - const interfaceMembers = type.symbol.members as Map; - if (!interfaceMembers) { - throw new Error('Interface does not have any members'); - } - const name = (type.aliasSymbol || type.symbol).name; - const [declaration] = (type.aliasSymbol || type.symbol).declarations; - const sourceFile = declaration.getSourceFile(); - const file = relativeLink(this.basePath, sourceFile.fileName); - const { line } = sourceFile.getLineAndCharacterOfPosition( - declaration.getStart(), - ); - const docs = this.getNodeDocs(declaration); - - const membersAndTypes = Array.from(interfaceMembers.values()).flatMap( - fieldSymbol => this.getMemberInfo(name, fieldSymbol), - ); - - const members = membersAndTypes.map(t => t.member); - const dependentTypes = this.flattenTypes( - membersAndTypes.flatMap(t => t.dependentTypes), - ); - - return { name, docs, file, lineInFile: line + 1, members, dependentTypes }; - } - - /** - * Grab primitive values from an object literal expression. - */ - private getObjectPropertyLiteral( - objectLiteral: ts.ObjectLiteralExpression, - propertyName: string, - ): string { - const matchingProp = objectLiteral.properties.filter( - prop => - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === propertyName, - )[0] as ts.PropertyAssignment; - - if (!matchingProp) { - throw new Error(`no identifier found for property ${propertyName}`); - } - - const { initializer } = matchingProp; - if (!ts.isStringLiteral(initializer)) { - throw new Error(`no string literal for ${propertyName}`); - } - - return initializer.text; - } - - /** - * Get field definitions for a given symbol. - */ - private getMemberInfo( - parentName: string, - symbol: ts.Symbol, - ): { member: FieldInfo; dependentTypes: TypeInfo[] } { - const declaration = symbol.valueDeclaration; - - const { links, infos: dependentTypes } = - this.findAllTypeReferences(declaration); - - let type: FieldInfo['type'] = 'prop'; - if ( - ts.isPropertySignature(declaration) && - declaration.type && - ts.isFunctionTypeNode(declaration.type) - ) { - type = 'method'; - } else if (ts.isMethodSignature(declaration)) { - type = 'method'; - } - - return { - member: { - type, - name: symbol.name, - path: `${parentName}.${symbol.name}`, - text: declaration.getText().replace(/;$/, ''), - docs: this.getNodeDocs(declaration), - links: this.recalculateLinkOffsets(declaration, links), - }, - dependentTypes, - }; - } - - /** - * Recursively search for references to types that we care about and want to include in the documentation. - */ - private findAllTypeReferences = ( - node: ts.Node | undefined, - visited: Set = new Set(), - ): { links: TypeLink[]; infos: TypeInfo[] } => { - if (!node || visited.has(node)) { - return { links: [], infos: [] }; - } - // This makes sure we don't end up in a loop. - // It doesn't exclude repeated types, e.g. Array>, since we're exiting on duplicate nodes, not types. - visited.add(node); - - const children = node - .getChildren() - .flatMap(child => this.findAllTypeReferences(child, visited)); - - if (!ts.isTypeReferenceNode(node)) { - return { - links: children.flatMap(child => child.links), - infos: children.flatMap(child => child.infos), - }; - } - - const type = this.checker.getTypeFromTypeNode(node); - - const info = this.validateTypeDeclaration(type); - if (!info) { - return { - links: children.flatMap(child => child.links), - infos: children.flatMap(child => child.infos), - }; - } - const { symbol, declaration } = info; - - const typeRefs = this.findAllTypeReferences(declaration, visited); - - const sourceFile = declaration.getSourceFile(); - const { line } = sourceFile.getLineAndCharacterOfPosition( - declaration.getStart(), - ); - const file = relativeLink(this.basePath, sourceFile.fileName); - const typeInfo = { - id: (symbol as any).id, - name: symbol.name, - text: declaration.getText().replace(/;$/, ''), - docs: this.getNodeDocs(declaration), - path: `${file}/${symbol.name}`, - file, - lineInFile: line + 1, // TS line is 0-based - links: this.recalculateLinkOffsets(declaration, typeRefs.links), - children: typeRefs.infos, - }; - - const link = { - id: typeInfo.id, - path: typeInfo.path, - name: typeInfo.name, - location: [node.typeName.getStart(), node.typeName.getEnd()] as const, - }; - - return { - links: [link, ...children.flatMap(child => child.links)], - infos: [typeInfo, ...children.flatMap(child => child.infos)], - }; - }; - - /** - * Check if a given type declaration is one that we care about. - */ - private validateTypeDeclaration( - type: ts.Type | undefined, - ): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } { - if (!type) { - return undefined; - } - const symbol = type.aliasSymbol || type.symbol; - if (!symbol) { - return undefined; - } - - const [declaration] = symbol.declarations; - - // Don't generate standalone infos for type paramters - if (ts.isTypeParameterDeclaration(declaration)) { - return undefined; - } - - if (declaration.getSourceFile().fileName.includes('node_modules')) { - return undefined; - } - - return { symbol, declaration }; - } - - /** - * Flatten a tree of TypeInfos with children into a flat - * list of TypeInfos with duplicates removed. - */ - private flattenTypes(descs: TypeInfo[]): TypeInfo[] { - function getChildren(parent: TypeInfo): TypeInfo[] { - return [ - { - ...parent, - children: [], - }, - ...parent.children.flatMap(getChildren), - ]; - } - - const flatDescs = descs.flatMap(getChildren); - const seenTypes = new Set(); - return flatDescs.filter(desc => { - if (seenTypes.has(desc.id)) { - return false; - } - seenTypes.add(desc.id); - return true; - }); - } - - /** - * Calculate link positions within a block of code, with 0 being - * the first character in the block. - */ - private recalculateLinkOffsets( - parent: ts.Node, - links: TypeLink[], - ): TypeLink[] { - const parentStart = parent.getStart(); - return links.map(link => { - const [start, end] = link.location; - return { - ...link, - location: [start - parentStart, end - parentStart], - }; - }); - } -} diff --git a/packages/docgen/src/docgen/ApiDocsPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts deleted file mode 100644 index 1fd62c29cd..0000000000 --- a/packages/docgen/src/docgen/ApiDocsPrinter.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import sortSelector from './sortSelector'; -import { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types'; - -/** - * The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator, - * and prints it out as a markdown doc with custom code highlighting and links. - */ -export default class ApiDocPrinter { - printerFactory: () => MarkdownPrinter; - - constructor(printerFactory: () => MarkdownPrinter) { - this.printerFactory = printerFactory; - } - - /** - * Print an index file with all ApiRefs and what types they implement. - */ - printApiIndex(apiDocs: ApiDoc[]): Buffer { - const printer = this.printerFactory(); - - printer.header(1, 'Backstage Core Utility APIs'); - - printer.paragraph( - 'The following is a list of all Utility APIs defined by `@backstage/core`.', - 'They are available to use by plugins and components, and can be accessed ', - 'using the `useApi` hook, also provided by `@backstage/core`.', - 'For more information, see https://github.com/backstage/backstage/blob/master/docs/api/utility-apis.md.', - ); - - for (const api of apiDocs) { - printer.header(3, `${this.apiDisplayName(api)}`, api.id); - - printer.paragraph(api.description); - - const typeLinks = api.interfaceInfos.map( - i => `[${i.name}](${printer.pageLink(i.name)})`, - ); - printer.paragraph( - `Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join( - ', ', - )}`, - ); - - printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`); - } - - return printer.toBuffer(); - } - - /** - * Print documentation page for a type implemented by an ApiRef and - */ - printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer { - const printer = this.printerFactory(); - - printer.header(1, apiType.name); - - printer.paragraph( - `The ${apiType.name} type is defined at ${printer.srcLink(apiType)}.`, - ); - - const apiLinks = apiDocs - .filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name)) - .map(ad => { - const link = - printer.indexLink() + - printer.headerLink(this.apiDisplayName(ad), ad.id); - return `[${ad.name}](${link})`; - }); - - if (apiLinks.length === 1) { - printer.paragraph( - `The following Utility API implements this type: ${apiLinks}`, - ); - } else { - printer.paragraph(`The following Utility APIs implement this type:`); - for (const link of apiLinks) { - printer.text(` - ${link}`); - } - } - - printer.header(2, 'Members'); - - this.addInterfaceMembers(printer, apiType); - - if (apiType.dependentTypes.length) { - printer.header(2, 'Supporting types'); - printer.paragraph( - 'These types are part of the API declaration, but may not be unique to this API.', - ); - - this.addInterfaceTypes(printer, apiType); - } - - return printer.toBuffer(); - } - - private addInterfaceMembers( - printer: MarkdownPrinter, - apiType: InterfaceInfo, - ) { - for (const member of apiType.members) { - printer.header( - 3, - `${member.name}${member.type === 'method' ? '()' : ''}`, - member.path, - ); - - for (const doc of member.docs) { - printer.text(doc); - } - - printer.code(member); - } - } - - private addInterfaceTypes(printer: MarkdownPrinter, apiType: InterfaceInfo) { - for (const type of apiType.dependentTypes - .slice() - .sort(sortSelector(x => x.name))) { - printer.header(3, `${type.name}`, type.path); - - for (const doc of type.docs) { - printer.text(doc); - } - printer.code(type); - - printer.paragraph(`Defined at ${printer.srcLink(type)}.`); - - const usageLinks = [...apiType.members, ...apiType.dependentTypes] - .filter(member => { - return member.links.some(link => link.id === type.id); - }) - .map( - ({ name, path }) => `[${name}](${printer.headerLink(name, path)})`, - ); - - if (usageLinks.length) { - printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`); - } - } - } - - private apiDisplayName(api: ApiDoc): string { - return api.name.replace(/ApiRef$/, ''); - } -} diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts deleted file mode 100644 index b36895597e..0000000000 --- a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import GithubSlugger from 'github-slugger'; -import sortSelector from './sortSelector'; -import { MarkdownPrinter, TypeLink } from './types'; -import { execSync } from 'child_process'; - -// TODO(Rugvip): provide through options? -const GH_BASE_URL = 'https://github.com/backstage/backstage'; - -const COMMIT_SHA = - process.env.COMMIT_SHA || - execSync('git rev-parse HEAD').toString('utf8').trim(); - -/** - * The GithubMarkdownPrinter is a MarkdownPrinter for printing GitHub Flavored Markdown documents. - */ -export default class GithubMarkdownPrinter implements MarkdownPrinter { - private str: string = ''; - - private line(line: string = '') { - this.str += `${line}\n`; - } - - text(text: string) { - this.line(text); - this.line(); - } - - header(level: number, text: string) { - this.line(`${'#'.repeat(level)} ${text}`); - this.line(); - } - - headerLink(heading: string): string { - const slug = GithubSlugger.slug(heading); - return `#${slug}`; - } - - indexLink() { - return './README.md'; - } - - pageLink(name: string) { - return `./${name}.md`; - } - - srcLink( - { file, lineInFile }: { file: string; lineInFile: number }, - text?: string, - ): string { - const linkText = text ?? `${file}:${lineInFile}`; - const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; - return `[${linkText}](${href})`; - } - - paragraph(...text: string[]) { - this.line( - text - .join('\n') - .trim() - .split('\n') - .map(line => line.trim()) - .join('\n'), - ); - this.line(); - } - - code({ text, links = [] }: { text: string; links?: TypeLink[] }) { - this.line('
');
-    this.line(this.formatWithLinks({ text, links }));
-    this.line('
'); - this.line(); - } - - private escapeText: (text: string) => string = (() => { - const escapes: { [char in string]: string } = { - '&': '&', - '<': '<', - '>': '>', - }; - - return (text: string) => text.replace(/[&<>]/g, char => escapes[char]); - })(); - - private formatWithLinks({ - text, - links = [], - }: { - text: string; - links?: TypeLink[]; - }): string { - const sortedLinks = links.slice().sort(sortSelector(x => x.location[0])); - - sortedLinks.reduce((lastEnd, link) => { - if (link.location[0] <= lastEnd) { - throw new Error( - `overlapping link detected for ${link.path}, ${link.location}`, - ); - } - return link.location[1]; - }, -1); - - const parts: Array<{ text: string; link?: boolean }> = []; - - const endLocation = sortedLinks.reduce((prev, link) => { - const [start, end] = link.location; - parts.push( - { text: text.slice(prev, start) }, - { text: text.slice(start, end), link: true }, - ); - return end; - }, 0); - - parts.push({ text: text.slice(endLocation) }); - - return parts - .map(part => { - if (part.link) { - const link = this.headerLink(part.text); - return `${this.escapeText(part.text)}`; - } - return this.escapeText(part.text); - }) - .join(''); - } - - toBuffer(): Buffer { - return Buffer.from(this.str, 'utf8'); - } -} diff --git a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts deleted file mode 100644 index c9e3010dda..0000000000 --- a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import sortSelector from './sortSelector'; -import { Highlighter, MarkdownPrinter, TypeLink } from './types'; -import { execSync } from 'child_process'; - -// TODO(Rugvip): provide through options? -const GH_BASE_URL = 'https://github.com/backstage/backstage'; - -const COMMIT_SHA = - process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); - -/** - * The TechdocsMarkdownPrinter is a MarkdownPrinter for building TechDocs markdown documents. - */ -export default class TechdocsMarkdownPrinter implements MarkdownPrinter { - private str: string = ''; - private readonly highlighter: Highlighter; - - constructor(highlighter: Highlighter) { - this.highlighter = highlighter; - - // Remove line numbers from codeblocks - this.style('.linenodiv{ display: none }'); - } - - private line(line: string = '') { - this.str += `${line}\n`; - } - - text(text: string) { - this.line(text); - this.line(); - } - - style(style: string) { - this.line(''); - } - - header(level: number, text: string, id?: string) { - this.line(`${'#'.repeat(level)} ${text}${id ? ` {#${id}}` : ''}`); - this.line(); - } - - headerLink(heading: string, id?: string): string { - return `#${id ?? heading}`; - } - - indexLink() { - return '../'; - } - - pageLink(name: string) { - return `./${name}/`; - } - - srcLink( - { file, lineInFile }: { file: string; lineInFile: number }, - text?: string, - ): string { - const linkText = text ?? `${file}:${lineInFile}`; - const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; - return `[${linkText}](${href})`; - } - - paragraph(...text: string[]) { - this.line( - text - .join('\n') - .trim() - .split('\n') - .map(line => line.trim()) - .join('\n'), - ); - this.line(); - } - - code({ text, links = [] }: { text: string; links?: TypeLink[] }) { - this.line('
'); - this.line( - '
', - ); - this.line('
');
-    this.line(this.formatWithLinks({ text, links }));
-    this.line('
'); - this.line('
'); - this.line('
'); - } - - private escapeText: (text: string) => string = (() => { - const escapes: { [char in string]: string } = { - '&': '&', - '<': '<', - '>': '>', - }; - - return (text: string) => text.replace(/[&<>]/g, char => escapes[char]); - })(); - - private formatWithLinks({ - text, - links = [], - }: { - text: string; - links?: TypeLink[]; - }): string { - const sortedLinks = links.slice().sort(sortSelector(x => x.location[0])); - - sortedLinks.reduce((lastEnd, link) => { - if (link.location[0] <= lastEnd) { - throw new Error( - `overlapping link detected for ${link.path}, ${link.location}`, - ); - } - return link.location[1]; - }, -1); - - const parts: Array<{ text: string; path?: string }> = []; - - const endLocation = sortedLinks.reduce((prev, link) => { - const [start, end] = link.location; - parts.push( - { text: text.slice(prev, start) }, - { text: text.slice(start, end), path: link.path }, - ); - return end; - }, 0); - - parts.push({ text: text.slice(endLocation) }); - - return parts - .map(part => { - if (part.path) { - const link = this.headerLink(part.text, part.path); - return `${this.escapeText(part.text)}`; - } - return this.highlighter.highlight(this.escapeText(part.text)); - }) - .join(''); - } - - toBuffer(): Buffer { - return Buffer.from(this.str, 'utf8'); - } -} diff --git a/packages/docgen/src/docgen/TypeLocator.test.ts b/packages/docgen/src/docgen/TypeLocator.test.ts deleted file mode 100644 index 8988b25814..0000000000 --- a/packages/docgen/src/docgen/TypeLocator.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; -import TypeLocator from './TypeLocator'; -import { createMemProgram } from './testUtils'; - -describe('TypeLocator', () => { - it('should find a default export', () => { - const program = createMemProgram('export default class MyApi {}'); - - const typeLocator = TypeLocator.fromProgram(program, '/mem'); - - const apiType = typeLocator.getExportedType('/mem/index.ts'); - expect(apiType.symbol.name).toBe('default'); - const [declaration] = apiType.symbol.declarations; - expect(declaration.kind).toBe(ts.SyntaxKind.ClassDeclaration); - expect((declaration as ts.ClassDeclaration).name).toBeDefined(); - expect((declaration as ts.ClassDeclaration).name!.text).toBe('MyApi'); - }, 10000); - - it('should find api instance export', () => { - const program = createMemProgram( - ` - import MyApi from './type'; - - type MyApiType = {}; - - export const myApi = new MyApi(); - `, - { - '/mem/type.ts': 'export default class MyApi {}', - }, - ); - - const typeLocator = TypeLocator.fromProgram(program, '/mem'); - - const { apiInstances } = typeLocator.findExportedInstances({ - apiInstances: typeLocator.getExportedType('/mem/type.ts'), - }); - - expect(apiInstances.length).toBe(1); - const [apiInstance] = apiInstances; - expect(apiInstance.name).toBe('myApi'); - expect(apiInstance.source.fileName).toBe('/mem/index.ts'); - expect(apiInstance.args).toEqual([]); - - expect(apiInstance.typeArgs.length).toBe(1); - const [typeArg] = apiInstance.typeArgs; - expect(typeArg.kind).toBe(ts.SyntaxKind.TypeReference); - expect((typeArg as ts.TypeReferenceNode).typeName.getText()).toBe( - 'MyApiType', - ); - }); -}); diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts deleted file mode 100644 index adec0c2a42..0000000000 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; -import { ExportedInstance } from './types'; - -/** - * The TypeLocator is used to extract typescrint Type structures from a compiled program, - * as well as finding locations where types are used in according to a matching pattern. - * - * This is used to e.g. find exported APIs that we should generate documentation for. - */ -export default class TypeLocator { - static fromProgram(program: ts.Program, sourcePath: string) { - return new TypeLocator(program.getTypeChecker(), program, sourcePath); - } - - constructor( - private readonly checker: ts.TypeChecker, - private readonly program: ts.Program, - private readonly sourcePath: string, - ) {} - - /** - * Get the type of a symbol by export path and name - */ - getExportedType(path: string, exportedName: string = 'default'): ts.Type { - const source = this.program.getSourceFile(path); - if (!source) { - throw new Error(`Source not found for path '${path}'`); - } - const exported = this.checker.getExportsOfModule( - this.checker.getSymbolAtLocation(source)!, - ); - const [symbol] = exported.filter(e => e.name === exportedName); - if (!symbol) { - throw new Error(`No export '${exportedName}' found in ${path}`); - } - const type = this.checker.getTypeOfSymbolAtLocation(symbol, source); - return type; - } - - /** - * Find exported instances and return values from calls using the types - * provided in the lookup table. - */ - findExportedInstances(typeLookupTable: { - [key in T]: ts.Type; - }): { [key in T]: ExportedInstance[] } { - const docMap = new Map(); - for (const type of Object.values(typeLookupTable)) { - docMap.set(type, []); - } - - this.program.getSourceFiles().forEach(source => { - const inRoot = source.fileName.startsWith(this.sourcePath); - if (!inRoot) { - return; - } - ts.forEachChild(source, node => { - const decl = this.getExportedConstructorDeclaration(node); - if (!decl || !docMap.has(decl.constructorType)) { - return; - } - - docMap.get(decl.constructorType)!.push({ node, source, ...decl }); - }); - }); - - const result: { [key in T]?: ExportedInstance[] } = {}; - - for (const key in typeLookupTable) { - if (typeLookupTable.hasOwnProperty(key)) { - const type = typeLookupTable[key]; - result[key] = docMap.get(type)!; - } - } - - return result as { [key in T]: ExportedInstance[] }; - } - - private getExportedConstructorDeclaration(node: ts.Node): - | { - constructorType: ts.Type; - args: ts.Expression[]; - typeArgs: ts.TypeNode[]; - name: string; - } - | undefined { - if (!ts.isVariableStatement(node)) { - return undefined; - } - if ( - !node.modifiers || - !node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) - ) { - return undefined; - } - const { declarations } = node.declarationList; - if (declarations.length !== 1) { - return undefined; - } - - const [declaration] = declarations; - const { initializer, name } = declaration; - - if (!initializer || !name) { - return undefined; - } - if (!ts.isCallOrNewExpression(initializer)) { - return undefined; - } - if (!ts.isIdentifier(name)) { - return undefined; - } - - const constructorType = this.checker.getTypeAtLocation( - initializer.expression, - ); - const args = Array.from(initializer.arguments ?? []); - const typeNode = declaration.type; - const typeArgs = Array.from( - (typeNode && ts.isTypeReferenceNode(typeNode) - ? typeNode.typeArguments - : initializer.typeArguments) ?? [], - ); - - return { constructorType, args, typeArgs, name: name.text }; - } -} diff --git a/packages/docgen/src/docgen/TypescriptHighlighter.ts b/packages/docgen/src/docgen/TypescriptHighlighter.ts deleted file mode 100644 index ab70ea8110..0000000000 --- a/packages/docgen/src/docgen/TypescriptHighlighter.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Highlighter } from './types'; - -// Simple syntax highlighter that mimics hilite -export default class TypescriptHighlighter implements Highlighter { - private static readonly basicTypes = [ - 'boolean', - 'number', - 'string', - 'Array', - 'object', - 'Record', - 'Set', - 'Map', - 'true', - 'false', - 'null', - 'undefined', - 'void', - 'Promise', - 'any', - '[0-9\\.]+', - ]; - - // List of highlightings to apply, each with a match regex and the style that should be applied - private static readonly highlighters = [ - [/(\/\*\*?(?:.|\n)+?\*\/)/g, 'color: #60a0b0; font-style: italic'], // block comment - [/(\/\/.*)/gm, 'color: #60a0b0; font-style: italic'], // line comment - [/('[^']*?'|"[^"]*?")/g, 'color: #4070a0'], // string literals - [ - new RegExp( - `\\b(${TypescriptHighlighter.basicTypes.join('|')})\\b(?!:|,)`, - 'g', - ), - 'color: #902000', - ], // basic types - [ - /^((?:export\s)?(?:type\s)?(?:interface\s)?)/g, - 'color: #007020; font-weight: bold', - ], // keywords - ] as readonly [RegExp, string][]; - - highlight(fullText: string): string { - // Each part is either plain text that can be highlighted or text that is already highlighted - type HighlightPart = { text: string; highlighted?: boolean }; - - const painter = (regex: RegExp, style: string) => (part: HighlightPart) => { - if (part.highlighted) { - return [part]; - } - // Apply highlighting to all matches of the regex by splitting into parts - return part.text.split(regex).map((text, index) => { - // Odd parts are the ones that matched the regex and should be highlighted. - if (index % 2 === 1) { - return { - text: `${text}`, - highlighted: true, - }; - } - return { text }; - }); - }; - - // Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted - return TypescriptHighlighter.highlighters - .reduce( - (parts, highlighter) => parts.flatMap(painter(...highlighter)), - [{ text: fullText }], - ) - .map(({ text }) => text) - .join(''); - } -} diff --git a/packages/docgen/src/docgen/sortSelector.test.ts b/packages/docgen/src/docgen/sortSelector.test.ts deleted file mode 100644 index 277b06faa5..0000000000 --- a/packages/docgen/src/docgen/sortSelector.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import sortSelector from './sortSelector'; - -describe('sortSelector', () => { - it('should stable sort', () => { - const arr = [ - [3, 1], - [1, 2], - [1, 1], - [1, 3], - [2, 1], - ]; - - const sortedByFirst = arr.slice().sort(sortSelector(([first]) => first)); - expect(sortedByFirst).toEqual([ - [1, 2], - [1, 1], - [1, 3], - [2, 1], - [3, 1], - ]); - - const sortedBySecond = arr - .slice() - .sort(sortSelector(([, second]) => second)); - expect(sortedBySecond).toEqual([ - [3, 1], - [1, 1], - [2, 1], - [1, 2], - [1, 3], - ]); - }); -}); diff --git a/packages/docgen/src/docgen/sortSelector.ts b/packages/docgen/src/docgen/sortSelector.ts deleted file mode 100644 index e8cb577ba5..0000000000 --- a/packages/docgen/src/docgen/sortSelector.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * The sortSelector is a utility that makes a sort function by selecting the value to sort on with a lambda. - */ -export default function sortSelector( - selector: (x: T) => any, -): (a: T, b: T) => -1 | 1 | 0 { - return (a: T, b: T) => { - const aV = selector(a); - const bV = selector(b); - if (aV < bV) { - return -1; - } else if (aV > bV) { - return 1; - } - return 0; - }; -} diff --git a/packages/docgen/src/docgen/testUtils.ts b/packages/docgen/src/docgen/testUtils.ts deleted file mode 100644 index c449dd7461..0000000000 --- a/packages/docgen/src/docgen/testUtils.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; - -export function createMemProgram( - indexSource: string, - otherSourceFiles: { [fileName in string]: string } = {}, -): ts.Program { - const rootDir = '/mem'; - - const options = { noEmit: true }; - const baseHost = ts.createCompilerHost(options); - const files: { [fileName in string]: string } = { - [`${rootDir}/index.ts`]: indexSource, - ...otherSourceFiles, - }; - - // Custom compiler hosts that reads from a map of in-memory files, but - // falls back to reading from disc for ts libs etc. - const compilerHost: ts.CompilerHost = { - ...baseHost, - readFile(fileName): string | undefined { - if (fileName in files) { - return files[fileName]; - } - return baseHost.readFile(fileName); - }, - getCurrentDirectory(): string { - return rootDir; - }, - directoryExists(dir) { - if (dir === rootDir) { - return true; - } - if (baseHost.directoryExists) { - return baseHost.directoryExists(dir); - } - return false; - }, - fileExists(fileName: string): boolean { - return !!files[fileName] || baseHost.fileExists(fileName); - }, - getSourceFile(fileName, ...rest): ts.SourceFile | undefined { - const file = files[fileName]; - if (file) { - return ts.createSourceFile(fileName, file, ts.ScriptTarget.ES2017); - } - return baseHost.getSourceFile(fileName, ...rest); - }, - }; - - return ts.createProgram(['/mem/index.ts'], options, compilerHost); -} diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts deleted file mode 100644 index 60f3d35007..0000000000 --- a/packages/docgen/src/docgen/types.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ts from 'typescript'; - -/** - * A TypeLink is a link to a different type. - */ -export type TypeLink = { - // The ID of the linked type - id: number; - // The path to the type from the project root - path: string; - // The name of the type, to display - name: string; - // The location of the type name as it appears in it's parent text. - location: readonly [number, number]; -}; - -/** - * TypeInfo describes a TypeScript Type. - */ -export type TypeInfo = { - id: number; - name: string; - path: string; - file: string; - lineInFile: number; - text: string; - docs: string[]; - links: TypeLink[]; - children: TypeInfo[]; -}; - -/** - * FieldInfo describes a property or method in a documented API. - */ -export type FieldInfo = { - type: 'prop' | 'method'; - name: string; - path: string; - text: string; - docs: string[]; - links: TypeLink[]; -}; - -/** - * InterfaceInfo describes the type of a documented API. - */ -export type InterfaceInfo = { - name: string; - docs: string[]; - file: string; - lineInFile: number; - members: Array; - dependentTypes: TypeInfo[]; -}; - -/** - * ApiDoc describes a documented API. - */ -export type ApiDoc = { - id: string; - name: string; - description: string; - file: string; - lineInFile: number; - interfaceInfos: InterfaceInfo[]; -}; - -/** - * ExportedInstance describes an expression matching `export {name} = new {Contructor}<{typeArgs}...>({args}...)` - */ -export type ExportedInstance = { - node: ts.Node; - name: string; - source: ts.SourceFile; - args: Array; - typeArgs: Array; -}; - -export type Highlighter = { - highlight(test: string): string; -}; - -/** - * Markdown printer is an abstraction for printing markdown documents of different flavors. - */ -export type MarkdownPrinter = { - text(text: string): void; - header(level: number, text: string, id?: string): void; - paragraph(...text: string[]): void; - code(options: { text: string; links?: TypeLink[] }): void; - - headerLink(header: string, id?: string): string; - /** Link from pages to index */ - indexLink(): string; - /** Link from index to pages */ - pageLink(name: string): string; - srcLink( - { file, lineInFile }: { file: string; lineInFile: number }, - text?: string, - ): string; - - toBuffer(): Buffer; -}; diff --git a/packages/docgen/src/generate.ts b/packages/docgen/src/generate.ts deleted file mode 100644 index bbc18e48ab..0000000000 --- a/packages/docgen/src/generate.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as ts from 'typescript'; -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'; -import ApiDocPrinter from './docgen/ApiDocsPrinter'; -import TypescriptHighlighter from './docgen/TypescriptHighlighter'; -import GitHubMarkdownPrinter from './docgen/GitHubMarkdownPrinter'; -import TechdocsMarkdownPrinter from './docgen/TechdocsMarkdownPrinter'; - -const FORMATS = ['github', 'techdocs'] as const; - -export async function generate( - targetPath: string, - format: typeof FORMATS[number], -) { - if (!FORMATS.includes(format)) { - throw new TypeError( - `Invalid format, '${format}', must be one of ${FORMATS.join(', ')}`, - ); - } - - /* eslint-disable-next-line no-restricted-syntax */ - const rootDir = resolvePath(__dirname, '../../..'); - const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src'); - const targetDir = resolvePath(targetPath); - - const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json')); - - delete options.moduleResolution; - options.noEmit = true; - - const program = ts.createProgram([resolvePath(srcDir, 'index.ts')], options); - - const typeLocator = TypeLocator.fromProgram(program, srcDir); - - const { apis } = typeLocator.findExportedInstances({ - apis: typeLocator.getExportedType( - resolvePath(srcDir, 'index.ts'), - 'createApiRef', - ), - }); - - const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir); - const apiDocs = apis - .map(api => { - try { - return apiDocGenerator.toDoc(api); - } catch (error) { - throw new Error( - `Doc generation failed for API in ${api.source.fileName}, ${error.stack}`, - ); - } - }) - .sort(sortSelector(x => x.name)); - - const apiTypes = Object.values( - Object.fromEntries( - apiDocs.flatMap(d => d.interfaceInfos).map(i => [i.name, i]), - ), - ).sort(sortSelector(i => i.name)); - - if (format === 'techdocs') { - const docsDir = resolvePath(targetDir, 'docs'); - await fs.ensureDir(docsDir); - - const apiDocPrinter = new ApiDocPrinter( - () => new TechdocsMarkdownPrinter(new TypescriptHighlighter()), - ); - - await fs.writeFile( - joinPath(docsDir, 'README.md'), - apiDocPrinter.printApiIndex(apiDocs), - ); - - for (const apiType of Object.values(apiTypes)) { - const data = apiDocPrinter.printInterface(apiType, apiDocs); - - await fs.writeFile(joinPath(docsDir, `${apiType.name}.md`), data); - } - - await fs.writeFile( - resolvePath(targetDir, 'mkdocs.yml'), - [ - 'site_name: Backstage Core Utility API References', - 'nav:', - ` - API Index: 'README.md'`, - ...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`), - 'plugins:', - ' - techdocs-core', - ].join('\n'), - 'utf8', - ); - } else { - await fs.ensureDir(targetDir); - - const apiDocPrinter = new ApiDocPrinter(() => new GitHubMarkdownPrinter()); - - await fs.writeFile( - joinPath(targetDir, 'README.md'), - apiDocPrinter.printApiIndex(apiDocs), - ); - - for (const apiType of Object.values(apiTypes)) { - const data = apiDocPrinter.printInterface(apiType, apiDocs); - - await fs.writeFile(joinPath(targetDir, `${apiType.name}.md`), data); - } - } -} diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts deleted file mode 100644 index 755bc9cb98..0000000000 --- a/packages/docgen/src/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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[]) => { - /* eslint-disable-next-line no-restricted-syntax */ - 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]') - .option( - '--format ', - 'Output format, either techdocs or github [techdocs]', - ) - .action(async cmd => { - await generate(cmd.output ?? './dist', cmd.format ?? 'techdocs'); - }); - - 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); - }); - - program.parse(argv); -}; - -process.on('unhandledRejection', rejection => { - console.error(String(rejection)); - process.exit(1); -}); - -main(process.argv); diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 5e8393fc78..6b34b4a82b 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -95,7 +95,6 @@ const SKIPPED_PACKAGES = [ 'packages/cli', 'packages/codemods', 'packages/create-app', - 'packages/docgen', 'packages/e2e-test', 'packages/storybook', 'packages/techdocs-cli',