From c1e2a8dcc530b20ef65f90cc26857a0a20632dba Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 14 Jul 2020 21:16:00 +0200 Subject: [PATCH 01/54] docs(adr): Created an ADR for MSW --- ...adr007-use-msw-to-mock-service-requests.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md new file mode 100644 index 0000000000..bdb221cea3 --- /dev/null +++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md @@ -0,0 +1,60 @@ +# ADR007: Use MSW to mock http requests + +## Context + +Network request mocking can be a total pain sometimes, in all different types of +tests, unit tests to e2e tests always have their own implementation of mocking +these requests. There's been traction in the outer community towards using this +library to mock network requests by using an express style declaration for +routes. react-testing-library suggests using this library instead of mocking +fetch directly wether this be in a browser or in node. + +https://github.com/mswjs/msw + +## Decision + +Moving forward, we have decided that any `fetch` or `XMLHTTPRequest` that +happens, should be mocked by using `msw`. + +Here is an example: + +```ts +import { setupWorker, rest } from 'msw'; + +const worker = setupWorker( + rest.get('*/user/:userId', (req, res, ctx) => { + return res( + ctx.json({ + firstName: 'John', + lastName: 'Maverick', + }), + ); + }), +); + +// Start the Mock Service Worker +worker.start(); +``` + +and in a more real life scenario, taken from +[CatalogClient.test.ts](https://github.com/spotify/backstage/blob/f3245c4f8f0b6b2625c4a6d5d50161b612fb4757/plugins/catalog/src/api/CatalogClient.test.ts) + +```ts +beforeEach(() => { + server.use( + rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => { + return res(ctx.json(defaultResponse)); + }), + ); +}); + +it('should entities from correct endpoint', async () => { + const entities = await client.getEntities(); + expect(entities).toEqual(defaultResponse); +}); +``` + +## Consequences + +- A little more code to write +- Gradually will replace the codebase with `msw` From 191aa0262f4255aecab058589be9371de6a26047 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 15 Jul 2020 10:02:49 +0200 Subject: [PATCH 02/54] chore(docs): add to doc structure --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 46367873e2..5d688edb52 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ nav: - ADR004 - Module Export Structure: 'architecture-decisions/adr004-module-export-structure.md' - ADR005 - Catalog Core Entities: 'architecture-decisions/adr005-catalog-core-entities.md' - ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md' + - ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md' - Contribute: '../CONTRIBUTING.md' - Support: 'overview/support.md' - FAQ: FAQ.md From 6e8691e9980b0963b2663d095102c121d6e12e63 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 14 Jul 2020 21:04:20 +0200 Subject: [PATCH 03/54] packages: inital dump of docgen + copyright headers --- packages/docgen/.eslintrc.js | 6 + packages/docgen/src/compiler.ts | 111 +++++++ .../docgen/src/docgen/ApiDocGenerator.test.ts | 261 +++++++++++++++ packages/docgen/src/docgen/ApiDocGenerator.ts | 297 ++++++++++++++++++ packages/docgen/src/docgen/ApiDocPrinter.ts | 112 +++++++ packages/docgen/src/docgen/MarkdownPrinter.ts | 136 ++++++++ .../docgen/src/docgen/TypeLocator.test.ts | 68 ++++ packages/docgen/src/docgen/TypeLocator.ts | 129 ++++++++ .../src/docgen/TypescriptHighlighter.ts | 88 ++++++ packages/docgen/src/docgen/flatMap.test.ts | 28 ++ packages/docgen/src/docgen/flatMap.ts | 20 ++ .../docgen/src/docgen/sortSelector.test.ts | 49 +++ packages/docgen/src/docgen/sortSelector.ts | 34 ++ packages/docgen/src/docgen/testUtils.ts | 67 ++++ packages/docgen/src/docgen/types.ts | 96 ++++++ packages/docgen/src/index.ts | 17 + 16 files changed, 1519 insertions(+) create mode 100644 packages/docgen/.eslintrc.js create mode 100644 packages/docgen/src/compiler.ts create mode 100644 packages/docgen/src/docgen/ApiDocGenerator.test.ts create mode 100644 packages/docgen/src/docgen/ApiDocGenerator.ts create mode 100644 packages/docgen/src/docgen/ApiDocPrinter.ts create mode 100644 packages/docgen/src/docgen/MarkdownPrinter.ts create mode 100644 packages/docgen/src/docgen/TypeLocator.test.ts create mode 100644 packages/docgen/src/docgen/TypeLocator.ts create mode 100644 packages/docgen/src/docgen/TypescriptHighlighter.ts create mode 100644 packages/docgen/src/docgen/flatMap.test.ts create mode 100644 packages/docgen/src/docgen/flatMap.ts create mode 100644 packages/docgen/src/docgen/sortSelector.test.ts create mode 100644 packages/docgen/src/docgen/sortSelector.ts create mode 100644 packages/docgen/src/docgen/testUtils.ts create mode 100644 packages/docgen/src/docgen/types.ts create mode 100644 packages/docgen/src/index.ts diff --git a/packages/docgen/.eslintrc.js b/packages/docgen/.eslintrc.js new file mode 100644 index 0000000000..503c048748 --- /dev/null +++ b/packages/docgen/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], + rules: { + 'no-console': 0, + }, +}; diff --git a/packages/docgen/src/compiler.ts b/packages/docgen/src/compiler.ts new file mode 100644 index 0000000000..3a196ca709 --- /dev/null +++ b/packages/docgen/src/compiler.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { resolve, join, dirname } from 'path'; +import { promisify } from 'util'; +import fs from 'fs'; +import ApiDocGenerator from './docgen/ApiDocGenerator'; +import sortSelector from './docgen/sortSelector'; +import TypeLocator from './docgen/TypeLocator'; +import ApiDocPrinter from './docgen/ApiDocPrinter'; +import TypescriptHighlighter from './docgen/TypescriptHighlighter'; +import MarkdownPrinter from './docgen/MarkdownPrinter'; +import { sync as mkdirpSync } from 'mkdirp'; + +const writeFile = promisify(fs.writeFile); + +function loadOptions(path: string): ts.CompilerOptions { + let { extends: parent, compilerOptions }: any = require(path); + + if (!parent) { + return compilerOptions; + } + if (parent.startsWith('.')) { + parent = join(dirname(path), parent); + } + + return { ...loadOptions(parent), ...compilerOptions }; +} + +async function main() { + const rootDir = resolve(__dirname, '..'); + const srcDir = resolve(rootDir, 'src'); + const entrypoint = resolve(srcDir, 'index.js'); + const apiRefsDir = resolve(rootDir, 'docgen', 'build', 'api-refs'); + const mkdocsYaml = resolve(apiRefsDir, 'mkdocs.yml'); + + process.chdir(rootDir); + + const options = loadOptions('../tsconfig.json'); + + delete options.moduleResolution; + options.removeComments = false; + options.noEmit = true; + + const program = ts.createProgram([entrypoint], options); + + const typeLocator = TypeLocator.fromProgram(program); + + const { apis } = typeLocator.findExportedInstances({ + apis: typeLocator.getExportedType( + resolve(srcDir, 'core', 'api', 'ApiRef.ts'), + ), + }); + + const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir, srcDir); + + 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.id)); + + const apiDocPrinter = new ApiDocPrinter( + () => new MarkdownPrinter(new TypescriptHighlighter()), + ); + + mkdirpSync(resolve(apiRefsDir, 'docs')); + + await Promise.all( + apiDocs.map(apiDoc => { + const data = apiDocPrinter.print(apiDoc); + + return writeFile(join(apiRefsDir, 'docs', `${apiDoc.name}.md`), data); + }), + ); + + fs.writeFileSync( + mkdocsYaml, + [ + 'site_name: api-references', + 'nav:', + ...apiDocs.map(({ id, name }) => ` - ${id}: '${name}.md'`), + ].join('\n'), + 'utf8', + ); +} + +main().catch(error => { + console.error(error.stack || error); + process.exit(1); +}); diff --git a/packages/docgen/src/docgen/ApiDocGenerator.test.ts b/packages/docgen/src/docgen/ApiDocGenerator.test.ts new file mode 100644 index 0000000000..f30c67288d --- /dev/null +++ b/packages/docgen/src/docgen/ApiDocGenerator.test.ts @@ -0,0 +1,261 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, '/mem', '/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.source.fileName).toBe('/mem/index.ts'); + expect(doc.interfaceInfo.dependentTypes).toEqual([]); + expect(doc.interfaceInfo.docs).toEqual([]); + expect(doc.interfaceInfo.file).toBe('index.ts'); + expect(doc.interfaceInfo.lineInFile).toBe(4); + expect(doc.interfaceInfo.members).toEqual([]); + expect(doc.interfaceInfo.name).toBe('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); + + const { apiInstances } = typeLocator.findExportedInstances({ + apiInstances: typeLocator.getExportedType('/mem/type.ts'), + }); + + expect(apiInstances.length).toBe(1); + const [apiInstance] = apiInstances; + + const docGenerator = ApiDocGenerator.fromProgram(program, '/mem', '/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.source.fileName).toBe('/mem/index.ts'); + expect(doc.interfaceInfo.docs).toEqual(['MyApiType Docs']); + expect(doc.interfaceInfo.file).toBe('index.ts'); + expect(doc.interfaceInfo.lineInFile).toBe(24); + expect(doc.interfaceInfo.name).toBe('MyApiType'); + expect(doc.interfaceInfo.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.interfaceInfo.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 new file mode 100644 index 0000000000..e1d1e0f5f1 --- /dev/null +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -0,0 +1,297 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 } from 'path'; +import { + ExportedInstance, + ApiDoc, + InterfaceInfo, + FieldInfo, + TypeInfo, + TypeLink, +} from './types'; +import { flatMap } from './flatMap'; + +/** + * 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 = new Api({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, + basePath: string, + sourcePath: string, + ) { + return new ApiDocGenerator(program.getTypeChecker(), basePath, sourcePath); + } + + constructor( + private readonly checker: ts.TypeChecker, + private readonly basePath: string, + private readonly sourcePath: string, + ) {} + + 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 interfaceInfo = this.getInterfaceInfo(typeArgs[0]); + if (!interfaceInfo) { + throw new Error('api has no info type argument'); + } + + return { id, name, source, description, interfaceInfo }; + } + + 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; + } + + 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 = relative(this.basePath, sourceFile.fileName); + const { line } = sourceFile.getLineAndCharacterOfPosition( + declaration.getStart(), + ); + const docs = this.getNodeDocs(declaration); + + const membersAndTypes = flatMap( + Array.from(interfaceMembers.values()), + fieldSymbol => this.getMemberInfo(name, fieldSymbol), + ); + + const members = membersAndTypes.map(t => t.member); + const dependentTypes = this.flattenTypes( + flatMap(membersAndTypes, t => t.dependentTypes), + ); + + return { name, docs, file, lineInFile: line + 1, members, dependentTypes }; + } + + private getObjectPropertyLiteral( + objectLiteral: ts.ObjectLiteralExpression, + propertyName: string, + ): string { + const prop = objectLiteral.properties.filter( + prop => + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === propertyName, + )[0] as ts.PropertyAssignment; + + if (!prop) { + throw new Error(`no identifier found for property ${propertyName}`); + } + + const { initializer } = prop; + if (!ts.isStringLiteral(initializer)) { + throw new Error(`no string literal for ${propertyName}`); + } + + return initializer.text; + } + + 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, + }; + } + + 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 = flatMap(node.getChildren(), child => + this.findAllTypeReferences(child, visited), + ); + + if (!ts.isTypeReferenceNode(node)) { + return { + links: flatMap(children, child => child.links), + infos: flatMap(children, child => child.infos), + }; + } + + const type = this.checker.getTypeFromTypeNode(node); + + const info = this.validateTypeDeclaration(type); + if (!info) { + return { + links: flatMap(children, child => child.links), + infos: flatMap(children, 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 = relative(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.getStart(), node.getEnd()] as const, + }; + + return { + links: [link, ...flatMap(children, child => child.links)], + infos: [typeInfo, ...flatMap(children, child => child.infos)], + }; + }; + + private validateTypeDeclaration( + type: ts.Type | undefined, + ): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } { + if (!type) { + return; + } + const symbol = type.aliasSymbol || type.symbol; + if (!symbol) { + return; + } + + const [declaration] = symbol.declarations; + + // Don't generate standalone infos for type paramters + if (ts.isTypeParameterDeclaration(declaration)) { + return; + } + + if (!declaration.getSourceFile().fileName.startsWith(this.sourcePath)) { + return; + } + + return { symbol, declaration }; + } + + private flattenTypes(descs: TypeInfo[]): TypeInfo[] { + function getChildren(parent: TypeInfo): TypeInfo[] { + return [ + { + ...parent, + children: [], + }, + ...flatMap(parent.children, getChildren), + ]; + } + + const flatDescs = flatMap(descs, getChildren); + const seenTypes = new Set(); + return flatDescs.filter(desc => { + if (seenTypes.has(desc.id)) { + return false; + } + seenTypes.add(desc.id); + return true; + }); + } + + 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/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts new file mode 100644 index 0000000000..5824047ef7 --- /dev/null +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { execSync } from 'child_process'; +import MarkdownPrinter from './MarkdownPrinter'; +import sortSelector from './sortSelector'; +import { ApiDoc } from './types'; + +const GH_BASE_URL = 'https://github.com/spotify/backstage'; + +const COMMIT_SHA = + process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); + +/** + * 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; + } + + mkTypeLink({ file, lineInFile }: { file: string; lineInFile: number }) { + const text = `${file}:${lineInFile}`; + const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; + return `[${text}](${href}){:target="_blank"}`; + } + + print(apiDoc: ApiDoc): Buffer { + const printer = this.printerFactory(); + + // Remove line numbers from codeblocks + printer.style('.linenodiv{ display: none }'); + + printer.header(1, `shared/apis/${apiDoc.id}`); + + const ifInfo = apiDoc.interfaceInfo; + + if (ifInfo.docs.length) { + for (const doc of ifInfo.docs) { + printer.text(doc); + } + } else { + printer.paragraph(apiDoc.description); + } + + printer.header(2, 'API Interface'); + printer.paragraph( + `The API interface type is defined at ${this.mkTypeLink(ifInfo)} as ${ + ifInfo.name + }.`, + ); + printer.paragraph('All members of the interface are listed below.'); + + for (const member of ifInfo.members) { + printer.header( + 3, + `${member.name}${member.type === 'method' ? '()' : ''}`, + member.path, + ); + + for (const doc of member.docs) { + printer.text(doc); + } + + printer.codeWithLinks(member); + } + + if (ifInfo.dependentTypes.length) { + printer.header(2, 'Types'); + } + for (const type of ifInfo.dependentTypes + .slice() + .sort(sortSelector(x => x.name))) { + printer.header(3, `${type.name}`, type.path); + + for (const doc of type.docs) { + printer.text(doc); + } + printer.codeWithLinks(type); + + printer.paragraph(`Defined at ${this.mkTypeLink(type)}.`); + + const usageLinks = [...ifInfo.members, ...ifInfo.dependentTypes] + .filter(member => { + return member.links.some(link => link.id === type.id); + }) + .map(({ name, path }) => `[${name}](#${path})`); + + if (usageLinks.length) { + printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`); + } + } + + return printer.toBuffer(); + } +} diff --git a/packages/docgen/src/docgen/MarkdownPrinter.ts b/packages/docgen/src/docgen/MarkdownPrinter.ts new file mode 100644 index 0000000000..c33ecc76a8 --- /dev/null +++ b/packages/docgen/src/docgen/MarkdownPrinter.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, TypeLink } from './types'; + +/** + * The MarkdownPrinter is a helper class for building markdown documents. + */ +export default class MarkdownPrinter { + private str: string = ''; + private readonly highlighter: Highlighter; + + constructor(highlighter: Highlighter) { + this.highlighter = highlighter; + } + + 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(); + } + + paragraph(text: string) { + this.line( + text + .trim() + .split('\n') + .map(line => line.trim()) + .join('\n'), + ); + this.line(); + } + + code(syntax: string = '', block: string) { + this.line(`\`\`\`${syntax}`); + this.line(block); + this.line('```'); + this.line(); + } + + codeWithLinks({ 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 end = 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(end) }); + + return parts + .map(part => { + if (part.path) { + return `${this.escapeText(part.text)}`; + } else { + 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 new file mode 100644 index 0000000000..651fbf13b2 --- /dev/null +++ b/packages/docgen/src/docgen/TypeLocator.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * 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); + + 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); + + 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 new file mode 100644 index 0000000000..8342548e01 --- /dev/null +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + private readonly checker: ts.TypeChecker; + private readonly program: ts.Program; + + static fromProgram(program: ts.Program) { + return new TypeLocator(program.getTypeChecker(), program); + } + + constructor(checker: ts.TypeChecker, program: ts.Program) { + this.checker = checker; + this.program = program; + } + + 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; + } + + 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 => { + ts.forEachChild(source, node => { + const decl = this.getExportedConstructorDeclaration(node); + if (!decl || !docMap.has(decl.constructorType)) { + return; + } + + docMap.get(decl.constructorType)!.push({ + node, + name: decl.name, + source, + args: Array.from(decl.initializer.arguments || []), + typeArgs: Array.from(decl.initializer.typeArguments || []), + }); + }); + }); + + 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; initializer: ts.NewExpression; name: string } + | undefined { + if (!ts.isVariableStatement(node)) { + return; + } + if ( + !node.modifiers || + !node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) + ) { + return; + } + const { declarations } = node.declarationList; + if (declarations.length !== 1) { + return; + } + + const [declaration] = declarations; + const { initializer, name } = declaration; + + if (!initializer || !name) { + return; + } + if (!ts.isNewExpression(initializer)) { + return; + } + if (!ts.isIdentifier(name)) { + return; + } + + const constructorType = this.checker.getTypeAtLocation( + initializer.expression, + ); + return { constructorType, initializer, name: name.text }; + } +} diff --git a/packages/docgen/src/docgen/TypescriptHighlighter.ts b/packages/docgen/src/docgen/TypescriptHighlighter.ts new file mode 100644 index 0000000000..a16c9a2c28 --- /dev/null +++ b/packages/docgen/src/docgen/TypescriptHighlighter.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * 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'; +import { flatMap } from './flatMap'; + +// 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(text: 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) => flatMap(parts, painter(...highlighter)), [ + { text }, + ]) + .map(({ text }) => text) + .join(''); + } +} diff --git a/packages/docgen/src/docgen/flatMap.test.ts b/packages/docgen/src/docgen/flatMap.test.ts new file mode 100644 index 0000000000..6186863039 --- /dev/null +++ b/packages/docgen/src/docgen/flatMap.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { flatMap } from './flatMap'; + +describe('flatMap', () => { + it('should flat map', () => { + expect(flatMap([], x => x)).toEqual([]); + expect(flatMap([[]], x => x)).toEqual([]); + // This simple implementation only flattens once + expect(flatMap([[[]]], x => x)).toEqual([[]]); + + expect(flatMap([[1, 2], 3, [4, 5]], x => x)).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/packages/docgen/src/docgen/flatMap.ts b/packages/docgen/src/docgen/flatMap.ts new file mode 100644 index 0000000000..bdea672a41 --- /dev/null +++ b/packages/docgen/src/docgen/flatMap.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export function flatMap(array: T[], func: (t: T) => R[] | R): R[] { + const out = array.map(func); + return ([] as R[]).concat.apply([], out as R[]); +} diff --git a/packages/docgen/src/docgen/sortSelector.test.ts b/packages/docgen/src/docgen/sortSelector.test.ts new file mode 100644 index 0000000000..a3da3e2950 --- /dev/null +++ b/packages/docgen/src/docgen/sortSelector.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 new file mode 100644 index 0000000000..1ba7a7918d --- /dev/null +++ b/packages/docgen/src/docgen/sortSelector.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * 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) => { + let aV = selector(a); + let bV = selector(b); + if (aV < bV) { + return -1; + } else if (aV > bV) { + return 1; + } else { + return 0; + } + }; +} diff --git a/packages/docgen/src/docgen/testUtils.ts b/packages/docgen/src/docgen/testUtils.ts new file mode 100644 index 0000000000..552776016a --- /dev/null +++ b/packages/docgen/src/docgen/testUtils.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 new file mode 100644 index 0000000000..df9a5b93a0 --- /dev/null +++ b/packages/docgen/src/docgen/types.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2020 Spotify AB + * + * 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; + source: ts.SourceFile; + description: string; + interfaceInfo: 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; +}; diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts new file mode 100644 index 0000000000..d57c49f9f8 --- /dev/null +++ b/packages/docgen/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 './compiler'; From 1fc5f9f468ee013f336ac1b184f46e9f39429d18 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 14 Jul 2020 21:11:46 +0200 Subject: [PATCH 04/54] docgen: add package.json and bin entrypoint --- packages/docgen/bin/backstage-docgen | 34 +++++++++++++++++++ packages/docgen/package.json | 49 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100755 packages/docgen/bin/backstage-docgen create mode 100644 packages/docgen/package.json diff --git a/packages/docgen/bin/backstage-docgen b/packages/docgen/bin/backstage-docgen new file mode 100755 index 0000000000..2b445ca9af --- /dev/null +++ b/packages/docgen/bin/backstage-docgen @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +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, + compilerOptions: { + module: 'CommonJS', + }, + }); + + require('../src'); +} diff --git a/packages/docgen/package.json b/packages/docgen/package.json new file mode 100644 index 0000000000..0d25860e8e --- /dev/null +++ b/packages/docgen/package.json @@ -0,0 +1,49 @@ +{ + "name": "docgen", + "description": "Tool for generating API Documentation for itself", + "version": "0.1.1-alpha.13", + "private": true, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/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", + "test:e2e": "node e2e-test/cli-e2e-test.js", + "clean": "backstage-cli clean", + "start": "nodemon --" + }, + "bin": { + "backstage-docgen": "bin/backstage-docgen" + }, + "dependencies": { + "chalk": "^4.0.0", + "commander": "^4.1.1", + "fs-extra": "^9.0.0", + "typescript": "^3.9.3", + "ts-node": "^8.6.2" + }, + "devDependencies": { + "@types/fs-extra": "^9.0.1", + "@types/node": "^13.7.2", + "nodemon": "^2.0.2" + }, + "files": [ + "bin", + "dist" + ], + "nodemonConfig": { + "watch": "./src", + "exec": "bin/backstage-docgen", + "ext": "ts" + } +} From 833d3f4bb431809412aefb83732f3897260caac7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 14 Jul 2020 23:00:30 +0200 Subject: [PATCH 05/54] docgen: fix lint issues --- packages/docgen/src/compiler.ts | 12 ++++++------ packages/docgen/src/docgen/ApiDocGenerator.ts | 14 +++++++------- packages/docgen/src/docgen/MarkdownPrinter.ts | 7 +++---- packages/docgen/src/docgen/TypeLocator.ts | 12 ++++++------ .../docgen/src/docgen/TypescriptHighlighter.ts | 4 ++-- packages/docgen/src/docgen/sortSelector.ts | 7 +++---- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/docgen/src/compiler.ts b/packages/docgen/src/compiler.ts index 3a196ca709..61837ad1f8 100644 --- a/packages/docgen/src/compiler.ts +++ b/packages/docgen/src/compiler.ts @@ -17,28 +17,28 @@ import * as ts from 'typescript'; import { resolve, join, dirname } from 'path'; import { promisify } from 'util'; -import fs from 'fs'; +import fs from 'fs-extra'; import ApiDocGenerator from './docgen/ApiDocGenerator'; import sortSelector from './docgen/sortSelector'; import TypeLocator from './docgen/TypeLocator'; import ApiDocPrinter from './docgen/ApiDocPrinter'; import TypescriptHighlighter from './docgen/TypescriptHighlighter'; import MarkdownPrinter from './docgen/MarkdownPrinter'; -import { sync as mkdirpSync } from 'mkdirp'; const writeFile = promisify(fs.writeFile); function loadOptions(path: string): ts.CompilerOptions { - let { extends: parent, compilerOptions }: any = require(path); + const config: any = require(path); + let parent = config.extends as string | undefined; if (!parent) { - return compilerOptions; + return config.compilerOptions; } if (parent.startsWith('.')) { parent = join(dirname(path), parent); } - return { ...loadOptions(parent), ...compilerOptions }; + return { ...loadOptions(parent), ...config.compilerOptions }; } async function main() { @@ -84,7 +84,7 @@ async function main() { () => new MarkdownPrinter(new TypescriptHighlighter()), ); - mkdirpSync(resolve(apiRefsDir, 'docs')); + fs.ensureDirSync(resolve(apiRefsDir, 'docs')); await Promise.all( apiDocs.map(apiDoc => { diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index e1d1e0f5f1..1142f1155e 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -116,18 +116,18 @@ export default class ApiDocGenerator { objectLiteral: ts.ObjectLiteralExpression, propertyName: string, ): string { - const prop = objectLiteral.properties.filter( + const matchingProp = objectLiteral.properties.filter( prop => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === propertyName, )[0] as ts.PropertyAssignment; - if (!prop) { + if (!matchingProp) { throw new Error(`no identifier found for property ${propertyName}`); } - const { initializer } = prop; + const { initializer } = matchingProp; if (!ts.isStringLiteral(initializer)) { throw new Error(`no string literal for ${propertyName}`); } @@ -238,22 +238,22 @@ export default class ApiDocGenerator { type: ts.Type | undefined, ): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } { if (!type) { - return; + return undefined; } const symbol = type.aliasSymbol || type.symbol; if (!symbol) { - return; + return undefined; } const [declaration] = symbol.declarations; // Don't generate standalone infos for type paramters if (ts.isTypeParameterDeclaration(declaration)) { - return; + return undefined; } if (!declaration.getSourceFile().fileName.startsWith(this.sourcePath)) { - return; + return undefined; } return { symbol, declaration }; diff --git a/packages/docgen/src/docgen/MarkdownPrinter.ts b/packages/docgen/src/docgen/MarkdownPrinter.ts index c33ecc76a8..36e7c8b1d6 100644 --- a/packages/docgen/src/docgen/MarkdownPrinter.ts +++ b/packages/docgen/src/docgen/MarkdownPrinter.ts @@ -108,7 +108,7 @@ export default class MarkdownPrinter { const parts: Array<{ text: string; path?: string }> = []; - const end = sortedLinks.reduce((prev, link) => { + const endLocation = sortedLinks.reduce((prev, link) => { const [start, end] = link.location; parts.push( { text: text.slice(prev, start) }, @@ -117,15 +117,14 @@ export default class MarkdownPrinter { return end; }, 0); - parts.push({ text: text.slice(end) }); + parts.push({ text: text.slice(endLocation) }); return parts .map(part => { if (part.path) { return `${this.escapeText(part.text)}`; - } else { - return this.highlighter.highlight(this.escapeText(part.text)); } + return this.highlighter.highlight(this.escapeText(part.text)); }) .join(''); } diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index 8342548e01..b960640559 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -95,30 +95,30 @@ export default class TypeLocator { | { constructorType: ts.Type; initializer: ts.NewExpression; name: string } | undefined { if (!ts.isVariableStatement(node)) { - return; + return undefined; } if ( !node.modifiers || !node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) ) { - return; + return undefined; } const { declarations } = node.declarationList; if (declarations.length !== 1) { - return; + return undefined; } const [declaration] = declarations; const { initializer, name } = declaration; if (!initializer || !name) { - return; + return undefined; } if (!ts.isNewExpression(initializer)) { - return; + return undefined; } if (!ts.isIdentifier(name)) { - return; + return undefined; } const constructorType = this.checker.getTypeAtLocation( diff --git a/packages/docgen/src/docgen/TypescriptHighlighter.ts b/packages/docgen/src/docgen/TypescriptHighlighter.ts index a16c9a2c28..4bbb6548dd 100644 --- a/packages/docgen/src/docgen/TypescriptHighlighter.ts +++ b/packages/docgen/src/docgen/TypescriptHighlighter.ts @@ -56,7 +56,7 @@ export default class TypescriptHighlighter implements Highlighter { ], // keywords ] as readonly [RegExp, string][]; - highlight(text: string): 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 }; @@ -80,7 +80,7 @@ export default class TypescriptHighlighter implements Highlighter { // Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted return TypescriptHighlighter.highlighters .reduce((parts, highlighter) => flatMap(parts, painter(...highlighter)), [ - { text }, + { text: fullText }, ]) .map(({ text }) => text) .join(''); diff --git a/packages/docgen/src/docgen/sortSelector.ts b/packages/docgen/src/docgen/sortSelector.ts index 1ba7a7918d..7590ae39ec 100644 --- a/packages/docgen/src/docgen/sortSelector.ts +++ b/packages/docgen/src/docgen/sortSelector.ts @@ -21,14 +21,13 @@ export default function sortSelector( selector: (x: T) => any, ): (a: T, b: T) => -1 | 1 | 0 { return (a: T, b: T) => { - let aV = selector(a); - let bV = selector(b); + const aV = selector(a); + const bV = selector(b); if (aV < bV) { return -1; } else if (aV > bV) { return 1; - } else { - return 0; } + return 0; }; } From 5a05761afec68a10470dca2f091868ea213ac959 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jul 2020 11:43:41 +0200 Subject: [PATCH 06/54] docgen: only use type name as link text --- packages/docgen/src/docgen/ApiDocGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 1142f1155e..7aaf1463da 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -225,7 +225,7 @@ export default class ApiDocGenerator { id: typeInfo.id, path: typeInfo.path, name: typeInfo.name, - location: [node.getStart(), node.getEnd()] as const, + location: [node.typeName.getStart(), node.typeName.getEnd()] as const, }; return { From ab450c3ad71dd4ff423c3c76ccb453b2a5076ee7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jul 2020 11:45:38 +0200 Subject: [PATCH 07/54] docgen: update paths and add filtering based on root paths --- packages/docgen/src/compiler.ts | 19 ++++++++++--------- packages/docgen/src/docgen/TypeLocator.ts | 5 +++++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/docgen/src/compiler.ts b/packages/docgen/src/compiler.ts index 61837ad1f8..74ba1db974 100644 --- a/packages/docgen/src/compiler.ts +++ b/packages/docgen/src/compiler.ts @@ -43,14 +43,14 @@ function loadOptions(path: string): ts.CompilerOptions { async function main() { const rootDir = resolve(__dirname, '..'); - const srcDir = resolve(rootDir, 'src'); - const entrypoint = resolve(srcDir, 'index.js'); - const apiRefsDir = resolve(rootDir, 'docgen', 'build', 'api-refs'); + 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 = loadOptions('../../../tsconfig.json'); delete options.moduleResolution; options.removeComments = false; @@ -60,11 +60,12 @@ async function main() { const typeLocator = TypeLocator.fromProgram(program); - const { apis } = typeLocator.findExportedInstances({ - apis: typeLocator.getExportedType( - resolve(srcDir, 'core', 'api', 'ApiRef.ts'), - ), - }); + const { apis } = typeLocator.findExportedInstances( + { + apis: typeLocator.getExportedType(entrypoint, 'createApiRef'), + }, + [srcDir], + ); const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir, srcDir); diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index b960640559..038a01d35e 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -54,6 +54,7 @@ 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)) { @@ -61,6 +62,10 @@ export default class TypeLocator { } this.program.getSourceFiles().forEach(source => { + const inRoot = roots.some(root => source.fileName.startsWith(root)); + if (!inRoot) { + return; + } ts.forEachChild(source, node => { const decl = this.getExportedConstructorDeclaration(node); if (!decl || !docMap.has(decl.constructorType)) { From e692991226615446037b03cadb6c61c96a73815e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jul 2020 11:47:30 +0200 Subject: [PATCH 08/54] docgen: add support for intersection types --- packages/docgen/src/docgen/ApiDocGenerator.ts | 14 +++++++++----- packages/docgen/src/docgen/ApiDocPrinter.ts | 4 ++-- packages/docgen/src/docgen/types.ts | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 7aaf1463da..8d0fd04240 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -61,12 +61,16 @@ export default class ApiDocGenerator { const id = this.getObjectPropertyLiteral(info, 'id'); const description = this.getObjectPropertyLiteral(info, 'description'); - const interfaceInfo = this.getInterfaceInfo(typeArgs[0]); - if (!interfaceInfo) { - throw new Error('api has no info type argument'); - } - return { id, name, source, description, interfaceInfo }; + const rootTypeNode = typeArgs[0]; + const typeNodes = ts.isIntersectionTypeNode(rootTypeNode) + ? rootTypeNode.types.slice() + : [rootTypeNode]; + const interfaceInfos = typeNodes.map(typeNode => + this.getInterfaceInfo(typeNode), + ); + + return { id, name, source, description, interfaceInfos }; } private getNodeDocs(node: ts.Node): string[] { diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 5824047ef7..5047885fc2 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -47,9 +47,9 @@ export default class ApiDocPrinter { // Remove line numbers from codeblocks printer.style('.linenodiv{ display: none }'); - printer.header(1, `shared/apis/${apiDoc.id}`); + printer.header(1, apiDoc.id); - const ifInfo = apiDoc.interfaceInfo; + const ifInfo = apiDoc.interfaceInfos[0]; if (ifInfo.docs.length) { for (const doc of ifInfo.docs) { diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index df9a5b93a0..b630caeda0 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -77,7 +77,7 @@ export type ApiDoc = { name: string; source: ts.SourceFile; description: string; - interfaceInfo: InterfaceInfo; + interfaceInfos: InterfaceInfo[]; }; /** From 7a5f4bcbd41c1b85feffdeb2d97b1d0c5b7e1633 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jul 2020 11:48:01 +0200 Subject: [PATCH 09/54] docgen: support both constructor and call expressions --- packages/docgen/src/docgen/TypeLocator.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index 038a01d35e..d5e7025f09 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -97,7 +97,11 @@ export default class TypeLocator { private getExportedConstructorDeclaration( node: ts.Node, ): - | { constructorType: ts.Type; initializer: ts.NewExpression; name: string } + | { + constructorType: ts.Type; + initializer: ts.CallExpression | ts.NewExpression; + name: string; + } | undefined { if (!ts.isVariableStatement(node)) { return undefined; @@ -119,7 +123,7 @@ export default class TypeLocator { if (!initializer || !name) { return undefined; } - if (!ts.isNewExpression(initializer)) { + if (!ts.isCallOrNewExpression(initializer)) { return undefined; } if (!ts.isIdentifier(name)) { From 2ec31c52642a81754bfa3cc11fb3b6486f5313a9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Jul 2020 12:08:32 +0200 Subject: [PATCH 10/54] docgen: remove flatMap util --- packages/docgen/src/docgen/ApiDocGenerator.ts | 32 +++++++++---------- .../src/docgen/TypescriptHighlighter.ts | 3 +- packages/docgen/src/docgen/flatMap.test.ts | 28 ---------------- packages/docgen/src/docgen/flatMap.ts | 20 ------------ 4 files changed, 16 insertions(+), 67 deletions(-) delete mode 100644 packages/docgen/src/docgen/flatMap.test.ts delete mode 100644 packages/docgen/src/docgen/flatMap.ts diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 8d0fd04240..01b1e0bf0f 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -24,7 +24,6 @@ import { TypeInfo, TypeLink, } from './types'; -import { flatMap } from './flatMap'; /** * The ApiDocGenerator uses the typescript compiler API to build the data structure that @@ -103,14 +102,13 @@ export default class ApiDocGenerator { ); const docs = this.getNodeDocs(declaration); - const membersAndTypes = flatMap( - Array.from(interfaceMembers.values()), - fieldSymbol => this.getMemberInfo(name, fieldSymbol), - ); + const membersAndTypes = Array.from( + interfaceMembers.values(), + ).flatMap(fieldSymbol => this.getMemberInfo(name, fieldSymbol)); const members = membersAndTypes.map(t => t.member); const dependentTypes = this.flattenTypes( - flatMap(membersAndTypes, t => t.dependentTypes), + membersAndTypes.flatMap(t => t.dependentTypes), ); return { name, docs, file, lineInFile: line + 1, members, dependentTypes }; @@ -184,14 +182,14 @@ export default class ApiDocGenerator { // It doesn't exclude repeated types, e.g. Array>, since we're exiting on duplicate nodes, not types. visited.add(node); - const children = flatMap(node.getChildren(), child => - this.findAllTypeReferences(child, visited), - ); + const children = node + .getChildren() + .flatMap(child => this.findAllTypeReferences(child, visited)); if (!ts.isTypeReferenceNode(node)) { return { - links: flatMap(children, child => child.links), - infos: flatMap(children, child => child.infos), + links: children.flatMap(child => child.links), + infos: children.flatMap(child => child.infos), }; } @@ -200,8 +198,8 @@ export default class ApiDocGenerator { const info = this.validateTypeDeclaration(type); if (!info) { return { - links: flatMap(children, child => child.links), - infos: flatMap(children, child => child.infos), + links: children.flatMap(child => child.links), + infos: children.flatMap(child => child.infos), }; } const { symbol, declaration } = info; @@ -233,8 +231,8 @@ export default class ApiDocGenerator { }; return { - links: [link, ...flatMap(children, child => child.links)], - infos: [typeInfo, ...flatMap(children, child => child.infos)], + links: [link, ...children.flatMap(child => child.links)], + infos: [typeInfo, ...children.flatMap(child => child.infos)], }; }; @@ -270,11 +268,11 @@ export default class ApiDocGenerator { ...parent, children: [], }, - ...flatMap(parent.children, getChildren), + ...parent.children.flatMap(getChildren), ]; } - const flatDescs = flatMap(descs, getChildren); + const flatDescs = descs.flatMap(getChildren); const seenTypes = new Set(); return flatDescs.filter(desc => { if (seenTypes.has(desc.id)) { diff --git a/packages/docgen/src/docgen/TypescriptHighlighter.ts b/packages/docgen/src/docgen/TypescriptHighlighter.ts index 4bbb6548dd..ffdd4f1906 100644 --- a/packages/docgen/src/docgen/TypescriptHighlighter.ts +++ b/packages/docgen/src/docgen/TypescriptHighlighter.ts @@ -15,7 +15,6 @@ */ import { Highlighter } from './types'; -import { flatMap } from './flatMap'; // Simple syntax highlighter that mimics hilite export default class TypescriptHighlighter implements Highlighter { @@ -79,7 +78,7 @@ export default class TypescriptHighlighter implements Highlighter { // Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted return TypescriptHighlighter.highlighters - .reduce((parts, highlighter) => flatMap(parts, painter(...highlighter)), [ + .reduce((parts, highlighter) => parts.flatMap(painter(...highlighter)), [ { text: fullText }, ]) .map(({ text }) => text) diff --git a/packages/docgen/src/docgen/flatMap.test.ts b/packages/docgen/src/docgen/flatMap.test.ts deleted file mode 100644 index 6186863039..0000000000 --- a/packages/docgen/src/docgen/flatMap.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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 { flatMap } from './flatMap'; - -describe('flatMap', () => { - it('should flat map', () => { - expect(flatMap([], x => x)).toEqual([]); - expect(flatMap([[]], x => x)).toEqual([]); - // This simple implementation only flattens once - expect(flatMap([[[]]], x => x)).toEqual([[]]); - - expect(flatMap([[1, 2], 3, [4, 5]], x => x)).toEqual([1, 2, 3, 4, 5]); - }); -}); diff --git a/packages/docgen/src/docgen/flatMap.ts b/packages/docgen/src/docgen/flatMap.ts deleted file mode 100644 index bdea672a41..0000000000 --- a/packages/docgen/src/docgen/flatMap.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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. - */ - -export function flatMap(array: T[], func: (t: T) => R[] | R): R[] { - const out = array.map(func); - return ([] as R[]).concat.apply([], out as R[]); -} From f668388c9e5268297300abc054f22395c158b20e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 11:43:06 +0200 Subject: [PATCH 11/54] docgen: restructure docs into index + type docs --- packages/docgen/src/compiler.ts | 27 ++++-- packages/docgen/src/docgen/ApiDocPrinter.ts | 96 +++++++++++++++++++-- 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/packages/docgen/src/compiler.ts b/packages/docgen/src/compiler.ts index 74ba1db974..ebb370f550 100644 --- a/packages/docgen/src/compiler.ts +++ b/packages/docgen/src/compiler.ts @@ -68,7 +68,6 @@ async function main() { ); const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir, srcDir); - const apiDocs = apis .map(api => { try { @@ -79,7 +78,13 @@ async function main() { ); } }) - .sort(sortSelector(x => x.id)); + .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)); const apiDocPrinter = new ApiDocPrinter( () => new MarkdownPrinter(new TypescriptHighlighter()), @@ -87,11 +92,16 @@ async function main() { fs.ensureDirSync(resolve(apiRefsDir, 'docs')); - await Promise.all( - apiDocs.map(apiDoc => { - const data = apiDocPrinter.print(apiDoc); + await writeFile( + join(apiRefsDir, 'docs', 'README.md'), + apiDocPrinter.printApiIndex(apiDocs), + ); - return writeFile(join(apiRefsDir, 'docs', `${apiDoc.name}.md`), data); + await Promise.all( + Object.values(apiTypes).map(apiType => { + const data = apiDocPrinter.printInterface(apiType, apiDocs); + + return writeFile(join(apiRefsDir, 'docs', `${apiType.name}.md`), data); }), ); @@ -100,7 +110,10 @@ async function main() { [ 'site_name: api-references', 'nav:', - ...apiDocs.map(({ id, name }) => ` - ${id}: '${name}.md'`), + ` - Utility API Index: 'README.md'`, + ...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`), + 'plugins:', + ' - techdocs-core', ].join('\n'), 'utf8', ); diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 5047885fc2..a97885430a 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -17,7 +17,7 @@ import { execSync } from 'child_process'; import MarkdownPrinter from './MarkdownPrinter'; import sortSelector from './sortSelector'; -import { ApiDoc } from './types'; +import { ApiDoc, InterfaceInfo } from './types'; const GH_BASE_URL = 'https://github.com/spotify/backstage'; @@ -67,7 +67,88 @@ export default class ApiDocPrinter { ); printer.paragraph('All members of the interface are listed below.'); - for (const member of ifInfo.members) { + this.addInterfaceMembers(printer, ifInfo); + + if (ifInfo.dependentTypes.length) { + printer.header(2, 'Types'); + + this.addInterfaceTypes(printer, ifInfo); + } + + return printer.toBuffer(); + } + + printApiIndex(apiDocs: ApiDoc[]): Buffer { + const printer = this.printerFactory(); + + printer.header(1, 'Backstage Utility APIs'); + + for (const api of apiDocs) { + printer.header(3, `${api.name.replace(/ApiRef$/, '')}`, api.id); + + printer.paragraph(api.description); + + const typeLinks = api.interfaceInfos.map(i => `[${i.name}](${i.name})`); + printer.paragraph( + `Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join( + ', ', + )}`, + ); + + printer.paragraph(`ApiRef: ${api.name}`); + } + + return printer.toBuffer(); + } + + printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer { + const printer = this.printerFactory(); + + // Remove line numbers from codeblocks + printer.style('.linenodiv{ display: none }'); + + printer.header(1, apiType.name); + + printer.paragraph( + `The ${apiType.name} type is defined at ${this.mkTypeLink(apiType)}.`, + ); + + const apiLinks = apiDocs + .filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name)) + .map(ad => `[${ad.name}](../#${ad.id})`); + + 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' ? '()' : ''}`, @@ -80,11 +161,10 @@ export default class ApiDocPrinter { printer.codeWithLinks(member); } + } - if (ifInfo.dependentTypes.length) { - printer.header(2, 'Types'); - } - for (const type of ifInfo.dependentTypes + 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); @@ -96,7 +176,7 @@ export default class ApiDocPrinter { printer.paragraph(`Defined at ${this.mkTypeLink(type)}.`); - const usageLinks = [...ifInfo.members, ...ifInfo.dependentTypes] + const usageLinks = [...apiType.members, ...apiType.dependentTypes] .filter(member => { return member.links.some(link => link.id === type.id); }) @@ -106,7 +186,5 @@ export default class ApiDocPrinter { printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`); } } - - return printer.toBuffer(); } } From b1558c5af7363508e97b54d586816bb67eff742a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:00:00 +0200 Subject: [PATCH 12/54] 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); From c5b2fdd84655be724c3fe082bf6bcd9d7060f16f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:07:45 +0200 Subject: [PATCH 13/54] docgen: read location and generate links for ApiRefs --- packages/docgen/src/docgen/ApiDocGenerator.ts | 13 ++++++++++++- packages/docgen/src/docgen/ApiDocPrinter.ts | 17 ++++++++++------- packages/docgen/src/docgen/types.ts | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 291c3f5b83..7e39a3c57d 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -55,6 +55,10 @@ export default class ApiDocGenerator { const id = this.getObjectPropertyLiteral(info, 'id'); const description = this.getObjectPropertyLiteral(info, 'description'); + const file = relative(this.sourcePath, source.fileName); + const { line } = source.getLineAndCharacterOfPosition( + apiInstance.node.getStart(), + ); const rootTypeNode = typeArgs[0]; const typeNodes = ts.isIntersectionTypeNode(rootTypeNode) @@ -64,7 +68,14 @@ export default class ApiDocGenerator { this.getInterfaceInfo(typeNode), ); - return { id, name, source, description, interfaceInfos }; + return { + id, + name, + file, + lineInFile: line + 1, + description, + interfaceInfos, + }; } private getNodeDocs(node: ts.Node): string[] { diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 43107a34ec..93c6f8677e 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -37,10 +37,13 @@ export default class ApiDocPrinter { this.printerFactory = printerFactory; } - mkTypeLink({ file, lineInFile }: { file: string; lineInFile: number }) { - const text = `${file}:${lineInFile}`; + mkLink( + { file, lineInFile }: { file: string; lineInFile: number }, + text?: string, + ) { + const linkText = text ?? `${file}:${lineInFile}`; const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${SRC_PATH}/${file}#L${lineInFile}`; - return `[${text}](${href}){:target="_blank"}`; + return `[${linkText}](${href}){:target="_blank"}`; } print(apiDoc: ApiDoc): Buffer { @@ -63,7 +66,7 @@ export default class ApiDocPrinter { printer.header(2, 'API Interface'); printer.paragraph( - `The API interface type is defined at ${this.mkTypeLink(ifInfo)} as ${ + `The API interface type is defined at ${this.mkLink(ifInfo)} as ${ ifInfo.name }.`, ); @@ -97,7 +100,7 @@ export default class ApiDocPrinter { )}`, ); - printer.paragraph(`ApiRef: ${api.name}`); + printer.paragraph(`ApiRef: ${this.mkLink(api, api.name)}`); } return printer.toBuffer(); @@ -112,7 +115,7 @@ export default class ApiDocPrinter { printer.header(1, apiType.name); printer.paragraph( - `The ${apiType.name} type is defined at ${this.mkTypeLink(apiType)}.`, + `The ${apiType.name} type is defined at ${this.mkLink(apiType)}.`, ); const apiLinks = apiDocs @@ -176,7 +179,7 @@ export default class ApiDocPrinter { } printer.codeWithLinks(type); - printer.paragraph(`Defined at ${this.mkTypeLink(type)}.`); + printer.paragraph(`Defined at ${this.mkLink(type)}.`); const usageLinks = [...apiType.members, ...apiType.dependentTypes] .filter(member => { diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index b630caeda0..7260b8697f 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -75,8 +75,9 @@ export type InterfaceInfo = { export type ApiDoc = { id: string; name: string; - source: ts.SourceFile; description: string; + file: string; + lineInFile: number; interfaceInfos: InterfaceInfo[]; }; From 2776f735d7cf79b1fe69a8c00b25d68be8464096 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:35:48 +0200 Subject: [PATCH 14/54] docgen: more explanation in api index --- packages/docgen/src/docgen/ApiDocPrinter.ts | 7 +++++++ packages/docgen/src/docgen/MarkdownPrinter.ts | 3 ++- packages/docgen/src/generate.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 93c6f8677e..221d50985e 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -88,6 +88,13 @@ export default class ApiDocPrinter { printer.header(1, 'Backstage 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 need to be provided by the app', + 'They can be accessed using the `useApi` hook, also provided by `@backstage/core`.', + 'For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.', + ); + for (const api of apiDocs) { printer.header(3, `${api.name.replace(/ApiRef$/, '')}`, api.id); diff --git a/packages/docgen/src/docgen/MarkdownPrinter.ts b/packages/docgen/src/docgen/MarkdownPrinter.ts index 36e7c8b1d6..fb64863da9 100644 --- a/packages/docgen/src/docgen/MarkdownPrinter.ts +++ b/packages/docgen/src/docgen/MarkdownPrinter.ts @@ -48,9 +48,10 @@ export default class MarkdownPrinter { this.line(); } - paragraph(text: string) { + paragraph(...text: string[]) { this.line( text + .join('\n') .trim() .split('\n') .map(line => line.trim()) diff --git a/packages/docgen/src/generate.ts b/packages/docgen/src/generate.ts index 59ac45b3ad..91ea3a02f6 100644 --- a/packages/docgen/src/generate.ts +++ b/packages/docgen/src/generate.ts @@ -87,7 +87,7 @@ export async function generate(targetPath: string) { [ 'site_name: api-references', 'nav:', - ` - Utility API Index: 'README.md'`, + ` - API Index: 'README.md'`, ...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`), 'plugins:', ' - techdocs-core', From 5078ef4541e258c242d75fc5d38428e3b5948a27 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:45:10 +0200 Subject: [PATCH 15/54] core-api: use type instead of interface to declare ConfigApi --- packages/core-api/src/apis/definitions/ConfigApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts index 63a588fc10..1fa6e70d1f 100644 --- a/packages/core-api/src/apis/definitions/ConfigApi.ts +++ b/packages/core-api/src/apis/definitions/ConfigApi.ts @@ -17,7 +17,7 @@ import { createApiRef } from '../ApiRef'; import { Config } from '@backstage/config'; // Using interface to make the ConfigApi name show up in docs -export interface ConfigApi extends Config {} +export type ConfigApi = Config; export const configApiRef = createApiRef({ id: 'core.config', From d8739638495e6484abefa183f45fa8e342d76fb6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:47:02 +0200 Subject: [PATCH 16/54] docgen: use repo root as base path but exclude types in node_modules --- packages/docgen/src/docgen/ApiDocGenerator.ts | 10 +++++----- packages/docgen/src/docgen/ApiDocPrinter.ts | 3 +-- packages/docgen/src/generate.ts | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 7e39a3c57d..8e697f42b0 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -42,7 +42,7 @@ export default class ApiDocGenerator { constructor( private readonly checker: ts.TypeChecker, - private readonly sourcePath: string, + private readonly basePath: string, ) {} toDoc(apiInstance: ExportedInstance): ApiDoc { @@ -55,7 +55,7 @@ export default class ApiDocGenerator { const id = this.getObjectPropertyLiteral(info, 'id'); const description = this.getObjectPropertyLiteral(info, 'description'); - const file = relative(this.sourcePath, source.fileName); + const file = relative(this.basePath, source.fileName); const { line } = source.getLineAndCharacterOfPosition( apiInstance.node.getStart(), ); @@ -102,7 +102,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.sourcePath, sourceFile.fileName); + const file = relative(this.basePath, sourceFile.fileName); const { line } = sourceFile.getLineAndCharacterOfPosition( declaration.getStart(), ); @@ -216,7 +216,7 @@ export default class ApiDocGenerator { const { line } = sourceFile.getLineAndCharacterOfPosition( declaration.getStart(), ); - const file = relative(this.sourcePath, sourceFile.fileName); + const file = relative(this.basePath, sourceFile.fileName); const typeInfo = { id: (symbol as any).id, name: symbol.name, @@ -260,7 +260,7 @@ export default class ApiDocGenerator { return undefined; } - if (!declaration.getSourceFile().fileName.startsWith(this.sourcePath)) { + if (declaration.getSourceFile().fileName.includes('node_modules')) { return undefined; } diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 221d50985e..2cb849e3d2 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -21,7 +21,6 @@ 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'); @@ -42,7 +41,7 @@ export default class ApiDocPrinter { text?: string, ) { const linkText = text ?? `${file}:${lineInFile}`; - const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${SRC_PATH}/${file}#L${lineInFile}`; + const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; return `[${linkText}](${href}){:target="_blank"}`; } diff --git a/packages/docgen/src/generate.ts b/packages/docgen/src/generate.ts index 91ea3a02f6..6cc95c58d2 100644 --- a/packages/docgen/src/generate.ts +++ b/packages/docgen/src/generate.ts @@ -25,8 +25,8 @@ import TypescriptHighlighter from './docgen/TypescriptHighlighter'; import MarkdownPrinter from './docgen/MarkdownPrinter'; export async function generate(targetPath: string) { - const rootDir = resolvePath(__dirname, '..'); - const srcDir = resolvePath(rootDir, '..', 'core-api', 'src'); + const rootDir = resolvePath(__dirname, '../../..'); + const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src'); const targetDir = resolvePath(targetPath); const docsDir = resolvePath(targetDir, 'docs'); @@ -46,7 +46,7 @@ export async function generate(targetPath: string) { ), }); - const apiDocGenerator = ApiDocGenerator.fromProgram(program, srcDir); + const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir); const apiDocs = apis .map(api => { try { From 4cd6774b029f5639f1062f89952de92dcdb73928 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 15:48:17 +0200 Subject: [PATCH 17/54] docgen: updated title --- packages/docgen/src/docgen/ApiDocPrinter.ts | 2 +- packages/docgen/src/generate.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocPrinter.ts index 2cb849e3d2..cd401e41b4 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocPrinter.ts @@ -85,7 +85,7 @@ export default class ApiDocPrinter { printApiIndex(apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); - printer.header(1, 'Backstage Utility APIs'); + printer.header(1, 'Backstage Core Utility APIs'); printer.paragraph( 'The following is a list of all Utility APIs defined by `@backstage/core`.', diff --git a/packages/docgen/src/generate.ts b/packages/docgen/src/generate.ts index 6cc95c58d2..3d9c1ba282 100644 --- a/packages/docgen/src/generate.ts +++ b/packages/docgen/src/generate.ts @@ -85,7 +85,7 @@ export async function generate(targetPath: string) { await fs.writeFile( resolvePath(targetDir, 'mkdocs.yml'), [ - 'site_name: api-references', + 'site_name: Backstage Core Utility API References', 'nav:', ` - API Index: 'README.md'`, ...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`), From 38b356f8268f8dd795565ff35e62113ed943deca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 16 Jul 2020 16:59:14 +0200 Subject: [PATCH 18/54] docgen: updated tests --- .../docgen/src/docgen/ApiDocGenerator.test.ts | 42 +++++++++++-------- .../docgen/src/docgen/TypeLocator.test.ts | 4 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.test.ts b/packages/docgen/src/docgen/ApiDocGenerator.test.ts index f30c67288d..947ce7ca04 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.test.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.test.ts @@ -39,7 +39,7 @@ describe('ApiDocGenerator', () => { }, ); - const typeLocator = TypeLocator.fromProgram(program); + const typeLocator = TypeLocator.fromProgram(program, '/'); const { apiInstances } = typeLocator.findExportedInstances({ apiInstances: typeLocator.getExportedType('/mem/type.ts'), @@ -48,19 +48,24 @@ describe('ApiDocGenerator', () => { expect(apiInstances.length).toBe(1); const [apiInstance] = apiInstances; - const docGenerator = ApiDocGenerator.fromProgram(program, '/mem', '/mem'); + 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.source.fileName).toBe('/mem/index.ts'); - expect(doc.interfaceInfo.dependentTypes).toEqual([]); - expect(doc.interfaceInfo.docs).toEqual([]); - expect(doc.interfaceInfo.file).toBe('index.ts'); - expect(doc.interfaceInfo.lineInFile).toBe(4); - expect(doc.interfaceInfo.members).toEqual([]); - expect(doc.interfaceInfo.name).toBe('MyApiType'); + 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', () => { @@ -132,7 +137,7 @@ describe('ApiDocGenerator', () => { return ids; }, {} as { [key in string]: number }); - const typeLocator = TypeLocator.fromProgram(program); + const typeLocator = TypeLocator.fromProgram(program, '/mem'); const { apiInstances } = typeLocator.findExportedInstances({ apiInstances: typeLocator.getExportedType('/mem/type.ts'), @@ -141,18 +146,19 @@ describe('ApiDocGenerator', () => { expect(apiInstances.length).toBe(1); const [apiInstance] = apiInstances; - const docGenerator = ApiDocGenerator.fromProgram(program, '/mem', '/mem'); + 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.source.fileName).toBe('/mem/index.ts'); - expect(doc.interfaceInfo.docs).toEqual(['MyApiType Docs']); - expect(doc.interfaceInfo.file).toBe('index.ts'); - expect(doc.interfaceInfo.lineInFile).toBe(24); - expect(doc.interfaceInfo.name).toBe('MyApiType'); - expect(doc.interfaceInfo.members).toEqual([ + 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', @@ -199,7 +205,7 @@ describe('ApiDocGenerator', () => { ], }, ]); - expect(doc.interfaceInfo.dependentTypes).toEqual([ + expect(doc.interfaceInfos[0].dependentTypes).toEqual([ { id: Ids.MySubType, name: 'MySubType', diff --git a/packages/docgen/src/docgen/TypeLocator.test.ts b/packages/docgen/src/docgen/TypeLocator.test.ts index 651fbf13b2..5a264fd41c 100644 --- a/packages/docgen/src/docgen/TypeLocator.test.ts +++ b/packages/docgen/src/docgen/TypeLocator.test.ts @@ -22,7 +22,7 @@ describe('TypeLocator', () => { it('should find a default export', () => { const program = createMemProgram('export default class MyApi {}'); - const typeLocator = TypeLocator.fromProgram(program); + const typeLocator = TypeLocator.fromProgram(program, '/mem'); const apiType = typeLocator.getExportedType('/mem/index.ts'); expect(apiType.symbol.name).toBe('default'); @@ -46,7 +46,7 @@ describe('TypeLocator', () => { }, ); - const typeLocator = TypeLocator.fromProgram(program); + const typeLocator = TypeLocator.fromProgram(program, '/mem'); const { apiInstances } = typeLocator.findExportedInstances({ apiInstances: typeLocator.getExportedType('/mem/type.ts'), From 9bbd1fed1871e85967f1fa98ba55074b3997881f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 00:22:16 +0200 Subject: [PATCH 19/54] docgen: add option to output github markdown --- .../{ApiDocPrinter.ts => ApiDocsPrinter.ts} | 84 ++--------- .../src/docgen/GitHubMarkdownPrinter.ts | 140 ++++++++++++++++++ ...nPrinter.ts => TechdocsMarkdownPrinter.ts} | 43 ++++-- packages/docgen/src/docgen/types.ts | 16 ++ packages/docgen/src/generate.ts | 85 +++++++---- packages/docgen/src/index.ts | 6 +- 6 files changed, 266 insertions(+), 108 deletions(-) rename packages/docgen/src/docgen/{ApiDocPrinter.ts => ApiDocsPrinter.ts} (64%) create mode 100644 packages/docgen/src/docgen/GitHubMarkdownPrinter.ts rename packages/docgen/src/docgen/{MarkdownPrinter.ts => TechdocsMarkdownPrinter.ts} (74%) diff --git a/packages/docgen/src/docgen/ApiDocPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts similarity index 64% rename from packages/docgen/src/docgen/ApiDocPrinter.ts rename to packages/docgen/src/docgen/ApiDocsPrinter.ts index cd401e41b4..d6c412d498 100644 --- a/packages/docgen/src/docgen/ApiDocPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocsPrinter.ts @@ -14,16 +14,8 @@ * limitations under the License. */ -import { execSync } from 'child_process'; -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 COMMIT_SHA = - process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); +import { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types'; /** * The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator, @@ -36,52 +28,6 @@ export default class ApiDocPrinter { this.printerFactory = printerFactory; } - mkLink( - { file, lineInFile }: { file: string; lineInFile: number }, - text?: string, - ) { - const linkText = text ?? `${file}:${lineInFile}`; - const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`; - return `[${linkText}](${href}){:target="_blank"}`; - } - - print(apiDoc: ApiDoc): Buffer { - const printer = this.printerFactory(); - - // Remove line numbers from codeblocks - printer.style('.linenodiv{ display: none }'); - - printer.header(1, apiDoc.id); - - const ifInfo = apiDoc.interfaceInfos[0]; - - if (ifInfo.docs.length) { - for (const doc of ifInfo.docs) { - printer.text(doc); - } - } else { - printer.paragraph(apiDoc.description); - } - - printer.header(2, 'API Interface'); - printer.paragraph( - `The API interface type is defined at ${this.mkLink(ifInfo)} as ${ - ifInfo.name - }.`, - ); - printer.paragraph('All members of the interface are listed below.'); - - this.addInterfaceMembers(printer, ifInfo); - - if (ifInfo.dependentTypes.length) { - printer.header(2, 'Types'); - - this.addInterfaceTypes(printer, ifInfo); - } - - return printer.toBuffer(); - } - printApiIndex(apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); @@ -89,8 +35,8 @@ export default class ApiDocPrinter { 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 need to be provided by the app', - 'They can be accessed using the `useApi` hook, also provided 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/spotify/backstage/blob/master/docs/api/utility-apis.md.', ); @@ -99,14 +45,14 @@ export default class ApiDocPrinter { printer.paragraph(api.description); - const typeLinks = api.interfaceInfos.map(i => `[${i.name}](${i.name})`); + const typeLinks = api.interfaceInfos.map(i => `[${i.name}](./${i.name})`); printer.paragraph( `Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join( ', ', )}`, ); - printer.paragraph(`ApiRef: ${this.mkLink(api, api.name)}`); + printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`); } return printer.toBuffer(); @@ -115,18 +61,18 @@ export default class ApiDocPrinter { printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); - // Remove line numbers from codeblocks - printer.style('.linenodiv{ display: none }'); - printer.header(1, apiType.name); printer.paragraph( - `The ${apiType.name} type is defined at ${this.mkLink(apiType)}.`, + `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 => `[${ad.name}](../#${ad.id})`); + .map(ad => { + const link = printer.indexLink() + printer.headerLink(ad.name, ad.id); + return `[${ad.name}](${link})`; + }); if (apiLinks.length === 1) { printer.paragraph( @@ -170,7 +116,7 @@ export default class ApiDocPrinter { printer.text(doc); } - printer.codeWithLinks(member); + printer.code(member); } } @@ -183,15 +129,17 @@ export default class ApiDocPrinter { for (const doc of type.docs) { printer.text(doc); } - printer.codeWithLinks(type); + printer.code(type); - printer.paragraph(`Defined at ${this.mkLink(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}](#${path})`); + .map( + ({ name, path }) => `[${name}](${printer.headerLink(name, path)})`, + ); if (usageLinks.length) { printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`); diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts new file mode 100644 index 0000000000..391f50a01d --- /dev/null +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -0,0 +1,140 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { MarkdownPrinter, TypeLink } from './types'; +import { execSync } from 'child_process'; + +// TODO(Rugvip): provide through options? +const GH_BASE_URL = 'https://github.com/spotify/backstage'; + +const COMMIT_SHA = + process.env.COMMIT_SHA || + execSync('git rev-parse HEAD').toString('utf8').trim(); + +/** + * The MarkdownPrinter is a helper class for building 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 = heading.replace(/[^a-z0-9]/gi, '-').toLowerCase(); + return `#${slug}`; + } + + indexLink() { + return './README.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/MarkdownPrinter.ts b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts similarity index 74% rename from packages/docgen/src/docgen/MarkdownPrinter.ts rename to packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts index fb64863da9..3609e0526d 100644 --- a/packages/docgen/src/docgen/MarkdownPrinter.ts +++ b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts @@ -15,17 +15,27 @@ */ import sortSelector from './sortSelector'; -import { Highlighter, TypeLink } from './types'; +import { Highlighter, MarkdownPrinter, TypeLink } from './types'; +import { execSync } from 'child_process'; + +// TODO(Rugvip): provide through options? +const GH_BASE_URL = 'https://github.com/spotify/backstage'; + +const COMMIT_SHA = + process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); /** * The MarkdownPrinter is a helper class for building markdown documents. */ -export default class MarkdownPrinter { +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 = '') { @@ -48,6 +58,23 @@ export default class MarkdownPrinter { this.line(); } + headerLink(heading: string, id?: string): string { + return `#${id ?? heading}`; + } + + indexLink() { + return '../'; + } + + 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 @@ -60,14 +87,7 @@ export default class MarkdownPrinter { this.line(); } - code(syntax: string = '', block: string) { - this.line(`\`\`\`${syntax}`); - this.line(block); - this.line('```'); - this.line(); - } - - codeWithLinks({ text, links = [] }: { text: string; links?: TypeLink[] }) { + code({ text, links = [] }: { text: string; links?: TypeLink[] }) { this.line('
'); this.line( '
', @@ -123,7 +143,8 @@ export default class MarkdownPrinter { return parts .map(part => { if (part.path) { - return `${this.escapeText(part.text)}`; + const link = this.headerLink(part.text, part.path); + return `${this.escapeText(part.text)}`; } return this.highlighter.highlight(this.escapeText(part.text)); }) diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index 7260b8697f..d091e9673e 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -95,3 +95,19 @@ export type ExportedInstance = { export type Highlighter = { highlight(test: string): string; }; + +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; + indexLink(): 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 index 3d9c1ba282..3587e2776b 100644 --- a/packages/docgen/src/generate.ts +++ b/packages/docgen/src/generate.ts @@ -20,15 +20,26 @@ 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/ApiDocPrinter'; +import ApiDocPrinter from './docgen/ApiDocsPrinter'; import TypescriptHighlighter from './docgen/TypescriptHighlighter'; -import MarkdownPrinter from './docgen/MarkdownPrinter'; +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(', ')}`, + ); + } -export async function generate(targetPath: string) { const rootDir = resolvePath(__dirname, '../../..'); const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src'); const targetDir = resolvePath(targetPath); - const docsDir = resolvePath(targetDir, 'docs'); const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json')); @@ -65,33 +76,51 @@ export async function generate(targetPath: string) { ), ).sort(sortSelector(i => i.name)); - const apiDocPrinter = new ApiDocPrinter( - () => new MarkdownPrinter(new TypescriptHighlighter()), - ); + if (format === 'techdocs') { + const docsDir = resolvePath(targetDir, 'docs'); + await fs.ensureDir(docsDir); - fs.ensureDirSync(docsDir); + const apiDocPrinter = new ApiDocPrinter( + () => new TechdocsMarkdownPrinter(new TypescriptHighlighter()), + ); - await fs.writeFile( - joinPath(docsDir, 'README.md'), - apiDocPrinter.printApiIndex(apiDocs), - ); + await fs.writeFile( + joinPath(docsDir, 'README.md'), + apiDocPrinter.printApiIndex(apiDocs), + ); - for (const apiType of Object.values(apiTypes)) { - const data = apiDocPrinter.printInterface(apiType, 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(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); + } } - - 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', - ); } diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts index 69f4ec31ad..866299151a 100644 --- a/packages/docgen/src/index.ts +++ b/packages/docgen/src/index.ts @@ -30,8 +30,12 @@ const main = (argv: string[]) => { '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'); + await generate(cmd.output ?? './dist', cmd.format ?? 'techdocs'); }); program.on('command:*', () => { From 6b6820bdd29109caa0fbaf4c00b258a09c66ae2a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 00:39:33 +0200 Subject: [PATCH 20/54] docgen: more docs --- packages/docgen/src/docgen/ApiDocGenerator.ts | 31 ++++++++++++++++++- packages/docgen/src/docgen/ApiDocsPrinter.ts | 6 ++++ .../src/docgen/GitHubMarkdownPrinter.ts | 2 +- .../src/docgen/TechdocsMarkdownPrinter.ts | 2 +- packages/docgen/src/docgen/TypeLocator.ts | 7 +++++ packages/docgen/src/docgen/types.ts | 3 ++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts index 8e697f42b0..bfe449155b 100644 --- a/packages/docgen/src/docgen/ApiDocGenerator.ts +++ b/packages/docgen/src/docgen/ApiDocGenerator.ts @@ -30,7 +30,7 @@ import { * describes a Backstage API and all of it's related types. * * It receives an exported instance that of the form - * `export name = new Api({id: ..., description: ...})`. + * `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. @@ -45,6 +45,9 @@ export default class ApiDocGenerator { private readonly basePath: string, ) {} + /** + * Generate documentation information for a given exported symbol. + */ toDoc(apiInstance: ExportedInstance): ApiDoc { const { name, source, args, typeArgs } = apiInstance; @@ -78,12 +81,18 @@ export default class ApiDocGenerator { }; } + /** + * 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( @@ -120,6 +129,9 @@ export default class ApiDocGenerator { return { name, docs, file, lineInFile: line + 1, members, dependentTypes }; } + /** + * Grab primitive values from an object literal expression. + */ private getObjectPropertyLiteral( objectLiteral: ts.ObjectLiteralExpression, propertyName: string, @@ -143,6 +155,9 @@ export default class ApiDocGenerator { return initializer.text; } + /** + * Get field definitions for a given symbol. + */ private getMemberInfo( parentName: string, symbol: ts.Symbol, @@ -177,6 +192,9 @@ export default class ApiDocGenerator { }; } + /** + * 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(), @@ -242,6 +260,9 @@ export default class ApiDocGenerator { }; }; + /** + * 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 } { @@ -267,6 +288,10 @@ export default class ApiDocGenerator { 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 [ @@ -289,6 +314,10 @@ export default class ApiDocGenerator { }); } + /** + * Calculate link positions within a block of code, with 0 being + * the first character in the block. + */ private recalculateLinkOffsets( parent: ts.Node, links: TypeLink[], diff --git a/packages/docgen/src/docgen/ApiDocsPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts index d6c412d498..05fcfbd141 100644 --- a/packages/docgen/src/docgen/ApiDocsPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocsPrinter.ts @@ -28,6 +28,9 @@ export default class ApiDocPrinter { this.printerFactory = printerFactory; } + /** + * Print an index file with all ApiRefs and what types they implement. + */ printApiIndex(apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); @@ -58,6 +61,9 @@ export default class ApiDocPrinter { return printer.toBuffer(); } + /** + * Print documentation page for a type implemented by an ApiRef and + */ printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer { const printer = this.printerFactory(); diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts index 391f50a01d..eb8e10c102 100644 --- a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -26,7 +26,7 @@ const COMMIT_SHA = execSync('git rev-parse HEAD').toString('utf8').trim(); /** - * The MarkdownPrinter is a helper class for building markdown documents. + * The GithubMarkdownPrinter is a MarkdownPrinter for printing Github-flavored markdown documents. */ export default class GithubMarkdownPrinter implements MarkdownPrinter { private str: string = ''; diff --git a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts index 3609e0526d..91fde3cd27 100644 --- a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts @@ -25,7 +25,7 @@ const COMMIT_SHA = process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8'); /** - * The MarkdownPrinter is a helper class for building markdown documents. + * The TechdocsMarkdownPrinter is a MarkdownPrinter for building TechDocs markdown documents. */ export default class TechdocsMarkdownPrinter implements MarkdownPrinter { private str: string = ''; diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index 164275b13f..6030258112 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -34,6 +34,9 @@ export default class TypeLocator { 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) { @@ -50,6 +53,10 @@ export default class TypeLocator { 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[] } { diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index d091e9673e..fae368b58d 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -96,6 +96,9 @@ 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; From 511ca47c27e826c30d4a340ac03c93423696f64d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:08:46 +0200 Subject: [PATCH 21/54] docgen: add docgen command to generate gfm docs --- package.json | 1 + packages/docgen/package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d702c4755..79af25a9af 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint": "lerna run lint --since origin/master --", "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", + "docgen": "lerna run docgen", "docker-build": "yarn bundle && docker build . -t spotify/backstage", "docker-build:all": "yarn tsc && yarn build && yarn docker-build && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 0d25860e8e..eafddbdda3 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -18,7 +18,7 @@ "build": "backstage-cli build --outputs cjs", "lint": "backstage-cli lint", "test": "backstage-cli test", - "test:e2e": "node e2e-test/cli-e2e-test.js", + "docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github", "clean": "backstage-cli clean", "start": "nodemon --" }, From 8cd5fb5e7c40142a0646b9d47c82961fe601f395 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:09:12 +0200 Subject: [PATCH 22/54] docs: generate utility api reference --- docs/reference/utility-apis/AlertApi.md | 115 +++++++++ docs/reference/utility-apis/AppThemeApi.md | 225 +++++++++++++++++ .../utility-apis/BackstageIdentityApi.md | 88 +++++++ docs/reference/utility-apis/Config.md | 179 ++++++++++++++ docs/reference/utility-apis/ErrorApi.md | 135 ++++++++++ .../reference/utility-apis/FeatureFlagsApi.md | 33 +++ docs/reference/utility-apis/IdentityApi.md | 81 ++++++ docs/reference/utility-apis/OAuthApi.md | 119 +++++++++ .../reference/utility-apis/OAuthRequestApi.md | 232 ++++++++++++++++++ .../utility-apis/OpenIdConnectApi.md | 75 ++++++ docs/reference/utility-apis/ProfileInfoApi.md | 94 +++++++ docs/reference/utility-apis/README.md | 135 ++++++++++ .../reference/utility-apis/SessionStateApi.md | 115 +++++++++ docs/reference/utility-apis/StorageApi.md | 186 ++++++++++++++ 14 files changed, 1812 insertions(+) create mode 100644 docs/reference/utility-apis/AlertApi.md create mode 100644 docs/reference/utility-apis/AppThemeApi.md create mode 100644 docs/reference/utility-apis/BackstageIdentityApi.md create mode 100644 docs/reference/utility-apis/Config.md create mode 100644 docs/reference/utility-apis/ErrorApi.md create mode 100644 docs/reference/utility-apis/FeatureFlagsApi.md create mode 100644 docs/reference/utility-apis/IdentityApi.md create mode 100644 docs/reference/utility-apis/OAuthApi.md create mode 100644 docs/reference/utility-apis/OAuthRequestApi.md create mode 100644 docs/reference/utility-apis/OpenIdConnectApi.md create mode 100644 docs/reference/utility-apis/ProfileInfoApi.md create mode 100644 docs/reference/utility-apis/README.md create mode 100644 docs/reference/utility-apis/SessionStateApi.md create mode 100644 docs/reference/utility-apis/StorageApi.md diff --git a/docs/reference/utility-apis/AlertApi.md b/docs/reference/utility-apis/AlertApi.md new file mode 100644 index 0000000000..b7d1a4d65d --- /dev/null +++ b/docs/reference/utility-apis/AlertApi.md @@ -0,0 +1,115 @@ +# AlertApi + +The AlertApi type is defined at +[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L29). + +The following Utility API implements this type: +[alertApiRef](./README.md#alertapiref) + +## Members + +### post() + +Post an alert for handling by the application. + +
+post(alert: AlertMessage): void
+
+ +### alert\$() + +Observe alerts posted by other parts of the application. + +
+alert$(): Observable<AlertMessage>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AlertMessage + +
+export type AlertMessage = {
+  message: string;
+  // Severity will default to success since that is what material ui defaults the value to.
+  severity?: 'success' | 'info' | 'warning' | 'error';
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L19). + +Referenced by: [post](#post), [alert\$](#alert-). + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [alert\$](#alert-). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md new file mode 100644 index 0000000000..79d41ef6f6 --- /dev/null +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -0,0 +1,225 @@ +# AppThemeApi + +The AppThemeApi type is defined at +[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50). + +The following Utility API implements this type: +[appThemeApiRef](./README.md#appthemeapiref) + +## Members + +### getInstalledThemes() + +Get a list of available themes. + +
+getInstalledThemes(): AppTheme[]
+
+ +### activeThemeId\$() + +Observe the currently selected theme. A value of undefined means no specific +theme has been selected. + +
+activeThemeId$(): Observable<string | undefined>
+
+ +### getActiveThemeId() + +Get the current theme ID. Returns undefined if no specific theme is selected. + +
+getActiveThemeId(): string | undefined
+
+ +### setActiveThemeId() + +Set a specific theme to use in the app, overriding the default theme selection. + +Clear the selection by passing in undefined. + +
+setActiveThemeId(themeId?: string): void
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AppTheme + +Describes a theme provided by the app. + +
+export type AppTheme = {
+  /**
+   * ID used to remember theme selections.
+   */
+  id: string;
+
+  /**
+   * Title of the theme
+   */
+  title: string;
+
+  /**
+   * Theme variant
+   */
+  variant: 'light' | 'dark';
+
+  /**
+   * The specialized MaterialUI theme instance.
+   */
+  theme: BackstageTheme;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24). + +Referenced by: [getInstalledThemes](#getinstalledthemes). + +### BackstagePalette + +
+export type BackstagePalette = Palette & PaletteAdditions
+
+ +Defined at +[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L63). + +Referenced by: [BackstageTheme](#backstagetheme). + +### BackstageTheme + +
+export interface BackstageTheme extends Theme {
+  palette: BackstagePalette;
+}
+
+ +Defined at +[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L66). + +Referenced by: [AppTheme](#apptheme). + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [activeThemeId\$](#activethemeid-). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### PaletteAdditions + +
+type PaletteAdditions = {
+  status: {
+    ok: string;
+    warning: string;
+    error: string;
+    pending: string;
+    running: string;
+    aborted: string;
+  };
+  border: string;
+  textContrast: string;
+  textVerySubtle: string;
+  textSubtle: string;
+  highlight: string;
+  errorBackground: string;
+  warningBackground: string;
+  infoBackground: string;
+  errorText: string;
+  infoText: string;
+  warningText: string;
+  linkHover: string;
+  link: string;
+  gold: string;
+  sidebar: string;
+  tabbar: {
+    indicator: string;
+  };
+  bursts: {
+    fontColor: string;
+    slackChannelText: string;
+    backgroundColor: {
+      default: string;
+    };
+  };
+  pinSidebarButton: {
+    icon: string;
+    background: string;
+  };
+}
+
+ +Defined at +[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L23). + +Referenced by: [BackstagePalette](#backstagepalette). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md new file mode 100644 index 0000000000..717a294621 --- /dev/null +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -0,0 +1,88 @@ +# BackstageIdentityApi + +The BackstageIdentityApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L144). + +The following Utility APIs implement this type: + +- [githubAuthApiRef](./README.md#githubauthapiref) + +- [gitlabAuthApiRef](./README.md#gitlabauthapiref) + +- [googleAuthApiRef](./README.md#googleauthapiref) + +- [oktaAuthApiRef](./README.md#oktaauthapiref) + +## Members + +### getBackstageIdentity() + +Get the user's identity within Backstage. This should normally not be called +directly, use the @IdentityApi instead. + +If the optional flag is not set, a session is guaranteed to be returned, while +if the optional flag is set, the session may be undefined. See +@AuthRequestOptions for more details. + +
+getBackstageIdentity(
+    options?: AuthRequestOptions,
+  ): Promise<BackstageIdentity | undefined>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AuthRequestOptions + +
+export type AuthRequestOptions = {
+  /**
+   * If this is set to true, the user will not be prompted to log in,
+   * and an empty response will be returned if there is no existing session.
+   *
+   * This can be used to perform a check whether the user is logged in, or if you don't
+   * want to force a user to be logged in, but provide functionality if they already are.
+   *
+   * @default false
+   */
+  optional?: boolean;
+
+  /**
+   * If this is set to true, the request will bypass the regular oauth login modal
+   * and open the login popup directly.
+   *
+   * The method must be called synchronously from a user action for this to work in all browsers.
+   *
+   * @default false
+   */
+  instantPopup?: boolean;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). + +Referenced by: [getBackstageIdentity](#getbackstageidentity). + +### BackstageIdentity + +
+export type BackstageIdentity = {
+  /**
+   * The backstage user ID.
+   */
+  id: string;
+
+  /**
+   * An ID token that can be used to authenticate the user within Backstage.
+   */
+  idToken: string;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L157). + +Referenced by: [getBackstageIdentity](#getbackstageidentity). diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md new file mode 100644 index 0000000000..ef53c5adcf --- /dev/null +++ b/docs/reference/utility-apis/Config.md @@ -0,0 +1,179 @@ +# Config + +The Config type is defined at +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L32). + +The following Utility API implements this type: +[configApiRef](./README.md#configapiref) + +## Members + +### keys() + +
+keys(): string[]
+
+ +### get() + +
+get(key: string): JsonValue
+
+ +### getOptional() + +
+getOptional(key: string): JsonValue | undefined
+
+ +### getConfig() + +
+getConfig(key: string): Config
+
+ +### getOptionalConfig() + +
+getOptionalConfig(key: string): Config | undefined
+
+ +### getConfigArray() + +
+getConfigArray(key: string): Config[]
+
+ +### getOptionalConfigArray() + +
+getOptionalConfigArray(key: string): Config[] | undefined
+
+ +### getNumber() + +
+getNumber(key: string): number
+
+ +### getOptionalNumber() + +
+getOptionalNumber(key: string): number | undefined
+
+ +### getBoolean() + +
+getBoolean(key: string): boolean
+
+ +### getOptionalBoolean() + +
+getOptionalBoolean(key: string): boolean | undefined
+
+ +### getString() + +
+getString(key: string): string
+
+ +### getOptionalString() + +
+getOptionalString(key: string): string | undefined
+
+ +### getStringArray() + +
+getStringArray(key: string): string[]
+
+ +### getOptionalStringArray() + +
+getOptionalStringArray(key: string): string[] | undefined
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### Config + +
+export type Config = {
+  keys(): string[];
+
+  get(key: string): JsonValue;
+  getOptional(key: string): JsonValue | undefined;
+
+  getConfig(key: string): Config;
+  getOptionalConfig(key: string): Config | undefined;
+
+  getConfigArray(key: string): Config[];
+  getOptionalConfigArray(key: string): Config[] | undefined;
+
+  getNumber(key: string): number;
+  getOptionalNumber(key: string): number | undefined;
+
+  getBoolean(key: string): boolean;
+  getOptionalBoolean(key: string): boolean | undefined;
+
+  getString(key: string): string;
+  getOptionalString(key: string): string | undefined;
+
+  getStringArray(key: string): string[];
+  getOptionalStringArray(key: string): string[] | undefined;
+}
+
+ +Defined at +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L32). + +Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig), +[getConfigArray](#getconfigarray), +[getOptionalConfigArray](#getoptionalconfigarray), [Config](#config). + +### JsonArray + +
+export type JsonArray = JsonValue[]
+
+ +Defined at +[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L18). + +Referenced by: [JsonValue](#jsonvalue). + +### JsonObject + +
+export type JsonObject = { [key in string]?: JsonValue }
+
+ +Defined at +[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L17). + +Referenced by: [JsonValue](#jsonvalue). + +### JsonValue + +
+export type JsonValue =
+  | JsonObject
+  | JsonArray
+  | number
+  | string
+  | boolean
+  | null
+
+ +Defined at +[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L19). + +Referenced by: [get](#get), [getOptional](#getoptional), +[JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config). diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md new file mode 100644 index 0000000000..efd4a3ed00 --- /dev/null +++ b/docs/reference/utility-apis/ErrorApi.md @@ -0,0 +1,135 @@ +# ErrorApi + +The ErrorApi type is defined at +[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). + +The following Utility API implements this type: +[errorApiRef](./README.md#errorapiref) + +## Members + +### post() + +Post an error for handling by the application. + +
+post(error: Error, context?: ErrorContext): void
+
+ +### error\$() + +Observe errors posted by other parts of the application. + +
+error$(): Observable<{ error: Error; context?: ErrorContext }>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### Error + +Mirrors the javascript Error class, for the purpose of providing documentation +and optional fields. + +
+type Error = {
+  name: string;
+  message: string;
+  stack?: string;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). + +Referenced by: [post](#post), [error\$](#error-). + +### ErrorContext + +Provides additional information about an error that was posted to the +application. + +
+export type ErrorContext = {
+  // If set to true, this error should not be displayed to the user. Defaults to false.
+  hidden?: boolean;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). + +Referenced by: [post](#post), [error\$](#error-). + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [error\$](#error-). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md new file mode 100644 index 0000000000..b3320b404a --- /dev/null +++ b/docs/reference/utility-apis/FeatureFlagsApi.md @@ -0,0 +1,33 @@ +# FeatureFlagsApi + +The FeatureFlagsApi type is defined at +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41). + +The following Utility API implements this type: +[featureFlagsApiRef](./README.md#featureflagsapiref) + +## Members + +### registeredFeatureFlags + +Store a list of registered feature flags. + +
+registeredFeatureFlags: FeatureFlagsRegistryItem[]
+
+ +### getFlags() + +Get a list of all feature flags from the current user. + +
+getFlags(): UserFlags
+
+ +### getRegisteredFlags() + +Get a list of all registered flags. + +
+getRegisteredFlags(): FeatureFlagsRegistry
+
diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md new file mode 100644 index 0000000000..f5de414cf5 --- /dev/null +++ b/docs/reference/utility-apis/IdentityApi.md @@ -0,0 +1,81 @@ +# IdentityApi + +The IdentityApi type is defined at +[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). + +The following Utility API implements this type: +[identityApiRef](./README.md#identityapiref) + +## Members + +### getUserId() + +The ID of the signed in user. This ID is not meant to be presented to the user, +but used as an opaque string to pass on to backends or use in frontend logic. + +TODO: The intention of the user ID is to be able to tie the user to an identity +that is known by the catalog and/or identity backend. It should for example be +possible to fetch all owned components using this ID. + +
+getUserId(): string
+
+ +### getProfile() + +The profile of the signed in user. + +
+getProfile(): ProfileInfo
+
+ +### getIdToken() + +An OpenID Connect ID Token which proves the identity of the signed in user. + +The ID token will be undefined if the signed in user does not have a verified +identity, such as a demo user or mocked user for e2e tests. + +
+getIdToken(): Promise<string | undefined>
+
+ +### logout() + +Log out the current user + +
+logout(): Promise<void>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### ProfileInfo + +Profile information of the user. + +
+export type ProfileInfo = {
+  /**
+   * Email ID.
+   */
+  email?: string;
+
+  /**
+   * Display name that can be presented to the user.
+   */
+  displayName?: string;
+
+  /**
+   * URL to an avatar image of the user.
+   */
+  picture?: string;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L172). + +Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md new file mode 100644 index 0000000000..e3334601f1 --- /dev/null +++ b/docs/reference/utility-apis/OAuthApi.md @@ -0,0 +1,119 @@ +# OAuthApi + +The OAuthApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L67). + +The following Utility APIs implement this type: + +- [githubAuthApiRef](./README.md#githubauthapiref) + +- [gitlabAuthApiRef](./README.md#gitlabauthapiref) + +- [googleAuthApiRef](./README.md#googleauthapiref) + +- [oauth2ApiRef](./README.md#oauth2apiref) + +- [oktaAuthApiRef](./README.md#oktaauthapiref) + +## Members + +### getAccessToken() + +Requests an OAuth 2 Access Token, optionally with a set of scopes. The access +token allows you to make requests on behalf of the user, and the copes may grant +you broader access, depending on the auth provider. + +Each auth provider has separate handling of scope, so you need to look at the +documentation for each one to know what scope you need to request. + +This method is cheap and should be called each time an access token is used. Do +not for example store the access token in React component state, as that could +cause the token to expire. Instead fetch a new access token for each request. + +Be sure to include all required scopes when requesting an access token. When +testing your implementation it is best to log out the Backstage session and then +visit your plugin page directly, as you might already have some required scopes +in your existing session. Not requesting the correct scopes can lead to 403 or +other authorization errors, which can be tricky to debug. + +If the user has not yet granted access to the provider and the set of requested +scopes, the user will be prompted to log in. The returned promise will not +resolve until the user has successfully logged in. The returned promise can be +rejected, but only if the user rejects the login request. + +
+getAccessToken(
+    scope?: OAuthScope,
+    options?: AuthRequestOptions,
+  ): Promise<string>
+
+ +### logout() + +Log out the user's session. This will reload the page. + +
+logout(): Promise<void>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AuthRequestOptions + +
+export type AuthRequestOptions = {
+  /**
+   * If this is set to true, the user will not be prompted to log in,
+   * and an empty response will be returned if there is no existing session.
+   *
+   * This can be used to perform a check whether the user is logged in, or if you don't
+   * want to force a user to be logged in, but provide functionality if they already are.
+   *
+   * @default false
+   */
+  optional?: boolean;
+
+  /**
+   * If this is set to true, the request will bypass the regular oauth login modal
+   * and open the login popup directly.
+   *
+   * The method must be called synchronously from a user action for this to work in all browsers.
+   *
+   * @default false
+   */
+  instantPopup?: boolean;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). + +Referenced by: [getAccessToken](#getaccesstoken). + +### OAuthScope + +This file contains declarations for common interfaces of auth-related APIs. The +declarations should be used to signal which type of authentication and +authorization methods each separate auth provider supports. + +For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect, +would be declared as follows: + +const googleAuthApiRef = createApiRef({ ... }) + +An array of scopes, or a scope string formatted according to the auth provider, +which is typically a space separated list. + +See the documentation for each auth provider for the list of scopes supported by +each provider. + +
+export type OAuthScope = string | string[]
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L38). + +Referenced by: [getAccessToken](#getaccesstoken). diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md new file mode 100644 index 0000000000..895c515fb0 --- /dev/null +++ b/docs/reference/utility-apis/OAuthRequestApi.md @@ -0,0 +1,232 @@ +# OAuthRequestApi + +The OAuthRequestApi type is defined at +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). + +The following Utility API implements this type: +[oauthRequestApiRef](./README.md#oauthrequestapiref) + +## Members + +### createAuthRequester() + +A utility for showing login popups or similar things, and merging together +multiple requests for different scopes into one request that inclues all scopes. + +The passed in options provide information about the login provider, and how to +handle auth requests. + +The returned AuthRequester function is used to request login with new scopes. +These requests are merged together and forwarded to the auth handler, as soon as +a consumer of auth requests triggers an auth flow. + +See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. + +
+createAuthRequester<AuthResponse>(
+    options: AuthRequesterOptions<AuthResponse>,
+  ): AuthRequester<AuthResponse>
+
+ +### authRequest\$() + +Observers panding auth requests. The returned observable will emit all current +active auth request, at most one for each created auth requester. + +Each request has its own info about the login provider, forwarded from the auth +requester options. + +Depending on user interaction, the request should either be rejected, or used to +trigger the auth handler. If the request is rejected, all pending AuthRequester +calls will fail with a "RejectedError". If a auth is triggered, and the auth +handler resolves successfully, then all currently pending AuthRequester calls +will resolve to the value returned by the onAuthRequest call. + +
+authRequest$(): Observable<PendingAuthRequest[]>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AuthProvider + +Information about the auth provider that we're requesting a login towards. + +This should be shown to the user so that they can be informed about what login +is being requested before a popup is shown. + +
+export type AuthProvider = {
+  /**
+   * Title for the auth provider, for example "GitHub"
+   */
+  title: string;
+
+  /**
+   * Icon for the auth provider.
+   */
+  icon: IconComponent;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). + +Referenced by: [AuthRequesterOptions](#authrequesteroptions), +[PendingAuthRequest](#pendingauthrequest). + +### AuthRequester + +Function used to trigger new auth requests for a set of scopes. + +The returned promise will resolve to the same value returned by the +onAuthRequest in the AuthRequesterOptions. Or rejected, if the request is +rejected. + +This function can be called multiple times before the promise resolves. All +calls will be merged into one request, and the scopes forwarded to the +onAuthRequest will be the union of all requested scopes. + +
+export type AuthRequester<AuthResponse> = (
+  scopes: Set<string>,
+) => Promise<AuthResponse>
+
+ +Defined at +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). + +Referenced by: [createAuthRequester](#createauthrequester). + +### AuthRequesterOptions + +Describes how to handle auth requests. Both how to show them to the user, and +what to do when the user accesses the auth request. + +
+export type AuthRequesterOptions<AuthResponse> = {
+  /**
+   * Information about the auth provider, which will be forwarded to auth requests.
+   */
+  provider: AuthProvider;
+
+  /**
+   * Implementation of the auth flow, which will be called synchronously when
+   * trigger() is called on an auth requests.
+   */
+  onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). + +Referenced by: [createAuthRequester](#createauthrequester). + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [authRequest\$](#authrequest-). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### PendingAuthRequest + +An pending auth request for a single auth provider. The request will remain in +this pending state until either reject() or trigger() is called. + +Any new requests for the same provider are merged into the existing pending +request, meaning there will only ever be a single pending request for a given +provider. + +
+export type PendingAuthRequest = {
+  /**
+   * Information about the auth provider, as given in the AuthRequesterOptions
+   */
+  provider: AuthProvider;
+
+  /**
+   * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
+   */
+  reject: () => void;
+
+  /**
+   * Trigger the auth request to continue the auth flow, by for example showing a popup.
+   *
+   * Synchronously calls onAuthRequest with all scope currently in the request.
+   */
+  trigger(): Promise<void>;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). + +Referenced by: [authRequest\$](#authrequest-). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md new file mode 100644 index 0000000000..c626ccf3a1 --- /dev/null +++ b/docs/reference/utility-apis/OpenIdConnectApi.md @@ -0,0 +1,75 @@ +# OpenIdConnectApi + +The OpenIdConnectApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L104). + +The following Utility APIs implement this type: + +- [googleAuthApiRef](./README.md#googleauthapiref) + +- [oauth2ApiRef](./README.md#oauth2apiref) + +- [oktaAuthApiRef](./README.md#oktaauthapiref) + +## Members + +### getIdToken() + +Requests an OpenID Connect ID Token. + +This method is cheap and should be called each time an ID token is used. Do not +for example store the id token in React component state, as that could cause the +token to expire. Instead fetch a new id token for each request. + +If the user has not yet logged in to Google inside Backstage, the user will be +prompted to log in. The returned promise will not resolve until the user has +successfully logged in. The returned promise can be rejected, but only if the +user rejects the login request. + +
+getIdToken(options?: AuthRequestOptions): Promise<string>
+
+ +### logout() + +Log out the user's session. This will reload the page. + +
+logout(): Promise<void>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AuthRequestOptions + +
+export type AuthRequestOptions = {
+  /**
+   * If this is set to true, the user will not be prompted to log in,
+   * and an empty response will be returned if there is no existing session.
+   *
+   * This can be used to perform a check whether the user is logged in, or if you don't
+   * want to force a user to be logged in, but provide functionality if they already are.
+   *
+   * @default false
+   */
+  optional?: boolean;
+
+  /**
+   * If this is set to true, the request will bypass the regular oauth login modal
+   * and open the login popup directly.
+   *
+   * The method must be called synchronously from a user action for this to work in all browsers.
+   *
+   * @default false
+   */
+  instantPopup?: boolean;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). + +Referenced by: [getIdToken](#getidtoken). diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md new file mode 100644 index 0000000000..6fcbc99950 --- /dev/null +++ b/docs/reference/utility-apis/ProfileInfoApi.md @@ -0,0 +1,94 @@ +# ProfileInfoApi + +The ProfileInfoApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L127). + +The following Utility APIs implement this type: + +- [githubAuthApiRef](./README.md#githubauthapiref) + +- [gitlabAuthApiRef](./README.md#gitlabauthapiref) + +- [googleAuthApiRef](./README.md#googleauthapiref) + +- [oauth2ApiRef](./README.md#oauth2apiref) + +- [oktaAuthApiRef](./README.md#oktaauthapiref) + +## Members + +### getProfile() + +Get profile information for the user as supplied by this auth provider. + +If the optional flag is not set, a session is guaranteed to be returned, while +if the optional flag is set, the session may be undefined. See +@AuthRequestOptions for more details. + +
+getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### AuthRequestOptions + +
+export type AuthRequestOptions = {
+  /**
+   * If this is set to true, the user will not be prompted to log in,
+   * and an empty response will be returned if there is no existing session.
+   *
+   * This can be used to perform a check whether the user is logged in, or if you don't
+   * want to force a user to be logged in, but provide functionality if they already are.
+   *
+   * @default false
+   */
+  optional?: boolean;
+
+  /**
+   * If this is set to true, the request will bypass the regular oauth login modal
+   * and open the login popup directly.
+   *
+   * The method must be called synchronously from a user action for this to work in all browsers.
+   *
+   * @default false
+   */
+  instantPopup?: boolean;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). + +Referenced by: [getProfile](#getprofile). + +### ProfileInfo + +Profile information of the user. + +
+export type ProfileInfo = {
+  /**
+   * Email ID.
+   */
+  email?: string;
+
+  /**
+   * Display name that can be presented to the user.
+   */
+  displayName?: string;
+
+  /**
+   * URL to an avatar image of the user.
+   */
+  picture?: string;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L172). + +Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md new file mode 100644 index 0000000000..398d55f605 --- /dev/null +++ b/docs/reference/utility-apis/README.md @@ -0,0 +1,135 @@ +# Backstage Core Utility APIs + +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/spotify/backstage/blob/master/docs/api/utility-apis.md. + +### alert + +Used to report alerts and forward them to the app + +Implemented type: [AlertApi](./AlertApi) + +ApiRef: +[alertApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L41) + +### appTheme + +API Used to configure the app theme, and enumerate options + +Implemented type: [AppThemeApi](./AppThemeApi) + +ApiRef: +[appThemeApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) + +### config + +Used to access runtime configuration + +Implemented type: [Config](./Config) + +ApiRef: +[configApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) + +### error + +Used to report errors and forward them to the app + +Implemented type: [ErrorApi](./ErrorApi) + +ApiRef: +[errorApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) + +### featureFlags + +Used to toggle functionality in features across Backstage + +Implemented type: [FeatureFlagsApi](./FeatureFlagsApi) + +ApiRef: +[featureFlagsApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) + +### githubAuth + +Provides authentication towards Github APIs + +Implemented types: [OAuthApi](./OAuthApi), [ProfileInfoApi](./ProfileInfoApi), +[BackstageIdentityApi](./BackstageIdentityApi), +[SessionStateApi](./SessionStateApi) + +ApiRef: +[githubAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L230) + +### gitlabAuth + +Provides authentication towards Gitlab APIs + +Implemented types: [OAuthApi](./OAuthApi), [ProfileInfoApi](./ProfileInfoApi), +[BackstageIdentityApi](./BackstageIdentityApi), +[SessionStateApi](./SessionStateApi) + +ApiRef: +[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L260) + +### googleAuth + +Provides authentication towards Google APIs and identities + +Implemented types: [OAuthApi](./OAuthApi), +[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), +[BackstageIdentityApi](./BackstageIdentityApi), +[SessionStateApi](./SessionStateApi) + +ApiRef: +[googleAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L213) + +### identity + +Provides access to the identity of the signed in user + +Implemented type: [IdentityApi](./IdentityApi) + +ApiRef: +[identityApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) + +### oauth2 + +Example of how to use oauth2 custom provider + +Implemented types: [OAuthApi](./OAuthApi), +[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), +[SessionStateApi](./SessionStateApi) + +ApiRef: +[oauth2ApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L270) + +### oauthRequest + +An API for implementing unified OAuth flows in Backstage + +Implemented type: [OAuthRequestApi](./OAuthRequestApi) + +ApiRef: +[oauthRequestApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) + +### oktaAuth + +Provides authentication towards Okta APIs + +Implemented types: [OAuthApi](./OAuthApi), +[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), +[BackstageIdentityApi](./BackstageIdentityApi), +[SessionStateApi](./SessionStateApi) + +ApiRef: +[oktaAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L243) + +### storage + +Provides the ability to store data which is unique to the user + +Implemented type: [StorageApi](./StorageApi) + +ApiRef: +[storageApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L68) diff --git a/docs/reference/utility-apis/SessionStateApi.md b/docs/reference/utility-apis/SessionStateApi.md new file mode 100644 index 0000000000..d15074aa9d --- /dev/null +++ b/docs/reference/utility-apis/SessionStateApi.md @@ -0,0 +1,115 @@ +# SessionStateApi + +The SessionStateApi type is defined at +[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L201). + +The following Utility APIs implement this type: + +- [githubAuthApiRef](./README.md#githubauthapiref) + +- [gitlabAuthApiRef](./README.md#gitlabauthapiref) + +- [googleAuthApiRef](./README.md#googleauthapiref) + +- [oauth2ApiRef](./README.md#oauth2apiref) + +- [oktaAuthApiRef](./README.md#oktaauthapiref) + +## Members + +### sessionState\$() + +
+sessionState$(): Observable<SessionState>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [sessionState\$](#sessionstate-). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### SessionState + +Session state values passed to subscribers of the SessionStateApi. + +
+export enum SessionState {
+  SignedIn = 'SignedIn',
+  SignedOut = 'SignedOut',
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L192). + +Referenced by: [sessionState\$](#sessionstate-). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md new file mode 100644 index 0000000000..dd366bcf18 --- /dev/null +++ b/docs/reference/utility-apis/StorageApi.md @@ -0,0 +1,186 @@ +# StorageApi + +The StorageApi type is defined at +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L31). + +The following Utility API implements this type: +[storageApiRef](./README.md#storageapiref) + +## Members + +### forBucket() + +Create a bucket to store data in. + +
+forBucket(name: string): StorageApi
+
+ +### get() + +Get the current value for persistent data, use observe\$ to be notified of +updates. + +
+get<T>(key: string): T | undefined
+
+ +### remove() + +Remove persistent data. + +
+remove(key: string): Promise<void>
+
+ +### set() + +Save persistant data, and emit messages to anyone that is using observe\$ for +this key + +
+set(key: string, data: any): Promise<void>
+
+ +### observe\$() + +Observe changes on a particular key in the bucket + +
+observe$<T>(key: string): Observable<StorageValueChange<T>>
+
+ +## Supporting types + +These types are part of the API declaration, but may not be unique to this API. + +### Observable + +Observable sequence of values and errors, see TC39. + +https://github.com/tc39/proposal-observable + +This is used as a common return type for observable values and can be created +using many different observable implementations, such as zen-observable or +RxJS 5. + +
+export type Observable<T> = {
+  /**
+   * Subscribes to this observable to start receiving new values.
+   */
+  subscribe(observer: Observer<T>): Subscription;
+  subscribe(
+    onNext: (value: T) => void,
+    onError?: (error: Error) => void,
+    onComplete?: () => void,
+  ): Subscription;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). + +Referenced by: [observe\$](#observe-), [StorageApi](#storageapi). + +### Observer + +This file contains non-react related core types used throught Backstage. + +Observer interface for consuming an Observer, see TC39. + +
+export type Observer<T> = {
+  next?(value: T): void;
+  error?(error: Error): void;
+  complete?(): void;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). + +Referenced by: [Observable](#observable). + +### StorageApi + +
+export interface StorageApi {
+  /**
+   * Create a bucket to store data in.
+   * @param {String} name Namespace for the storage to be stored under,
+   *                      will inherit previous namespaces too
+   */
+  forBucket(name: string): StorageApi;
+
+  /**
+   * Get the current value for persistent data, use observe$ to be notified of updates.
+   *
+   * @param {String} key Unique key associated with the data.
+   * @return {Object} data The data that should is stored.
+   */
+  get<T>(key: string): T | undefined;
+
+  /**
+   * Remove persistent data.
+   *
+   * @param {String} key Unique key associated with the data.
+   */
+  remove(key: string): Promise<void>;
+
+  /**
+   * Save persistant data, and emit messages to anyone that is using observe$ for this key
+   *
+   * @param {String} key Unique key associated with the data.
+   */
+  set(key: string, data: any): Promise<void>;
+
+  /**
+   * Observe changes on a particular key in the bucket
+   * @param {String} key Unique key associated with the data
+   */
+  observe$<T>(key: string): Observable<StorageValueChange<T>>;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L31). + +Referenced by: [forBucket](#forbucket). + +### StorageValueChange + +
+export type StorageValueChange<T = any> = {
+  key: string;
+  newValue?: T;
+}
+
+ +Defined at +[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L21). + +Referenced by: [observe\$](#observe-), [StorageApi](#storageapi). + +### Subscription + +Subscription returned when subscribing to an Observable, see TC39. + +
+export type Subscription = {
+  /**
+   * Cancels the subscription
+   */
+  unsubscribe(): void;
+
+  /**
+   * Value indicating whether the subscription is closed.
+   */
+  readonly closed: Boolean;
+}
+
+ +Defined at +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). + +Referenced by: [Observable](#observable). From 3dd7fa7c3e5b3740f5b0fdca20c9065e148948ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:16:05 +0200 Subject: [PATCH 23/54] docs: add links to api references --- docs/README.md | 3 ++- docs/api/utility-apis.md | 3 ++- docs/auth/index.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index 904fdf3356..c7721976c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,7 +76,8 @@ better yet, a pull request. - [Figma resources](dls/figma.md) - API references - TypeScript API - - [Utilities](api/utility-apis.md) + - [Utility APIs](api/utility-apis.md) + - [Utility API References](reference/utility-apis/README.md) - [createPlugin](reference/createPlugin.md) - [createPlugin-feature-flags](reference/createPlugin-feature-flags.md) - [createPlugin-router](reference/createPlugin-router.md) diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 2b15da0295..f87db15e7e 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -19,7 +19,8 @@ during their entire life cycle. Each Utility API is tied to an `ApiRef` instance, which is a global singleton object without any additional state or functionality, its only purpose is to reference Utility APIs. `ApiRef`s are create using `createApiRef`, which is -exported by `@backstage/core`. There are many predefined Utility APIs defined in +exported by `@backstage/core`. There are many +[predefined Utility APIs](../reference/utility-apis/README.md) defined in `@backstage/core`, and they're all exported with a name of the pattern `*ApiRef`, for example `errorApiRef`. diff --git a/docs/auth/index.md b/docs/auth/index.md index 0cb56501ab..6f9af17cba 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -26,7 +26,8 @@ OAuth helps in that regard. The method with which frontend plugins request access to third party services is through [Utility APIs](../api/utility-apis.md) for each service provider. For a -full list of providers, see [TODO](#TODO). +full list of providers, see the +[Utility API References](../reference/utility-apis/README.md). ### Identity - WIP From f5e5520cbc57a9b269216660e7d2d19f5507dc90 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:17:23 +0200 Subject: [PATCH 24/54] docgen: added README --- packages/docgen/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/docgen/README.md diff --git a/packages/docgen/README.md b/packages/docgen/README.md new file mode 100644 index 0000000000..be1d7cd521 --- /dev/null +++ b/packages/docgen/README.md @@ -0,0 +1,21 @@ +# 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. From 7e5451d8a9676e4b7fd3b2716a50680e8f83d5cb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:34:34 +0200 Subject: [PATCH 25/54] docgen: fix markdown links --- packages/docgen/src/docgen/ApiDocsPrinter.ts | 14 +++++++++++--- .../docgen/src/docgen/GitHubMarkdownPrinter.ts | 4 ++++ .../docgen/src/docgen/TechdocsMarkdownPrinter.ts | 4 ++++ packages/docgen/src/docgen/types.ts | 3 +++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/docgen/src/docgen/ApiDocsPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts index 05fcfbd141..fdd53afc8e 100644 --- a/packages/docgen/src/docgen/ApiDocsPrinter.ts +++ b/packages/docgen/src/docgen/ApiDocsPrinter.ts @@ -44,11 +44,13 @@ export default class ApiDocPrinter { ); for (const api of apiDocs) { - printer.header(3, `${api.name.replace(/ApiRef$/, '')}`, api.id); + printer.header(3, `${this.apiDisplayName(api)}`, api.id); printer.paragraph(api.description); - const typeLinks = api.interfaceInfos.map(i => `[${i.name}](./${i.name})`); + const typeLinks = api.interfaceInfos.map( + i => `[${i.name}](${printer.pageLink(i.name)})`, + ); printer.paragraph( `Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join( ', ', @@ -76,7 +78,9 @@ export default class ApiDocPrinter { const apiLinks = apiDocs .filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name)) .map(ad => { - const link = printer.indexLink() + printer.headerLink(ad.name, ad.id); + const link = + printer.indexLink() + + printer.headerLink(this.apiDisplayName(ad), ad.id); return `[${ad.name}](${link})`; }); @@ -152,4 +156,8 @@ export default class ApiDocPrinter { } } } + + 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 index eb8e10c102..fd82e023d3 100644 --- a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -54,6 +54,10 @@ export default class GithubMarkdownPrinter implements MarkdownPrinter { return './README.md'; } + pageLink(name: string) { + return `./${name}.md`; + } + srcLink( { file, lineInFile }: { file: string; lineInFile: number }, text?: string, diff --git a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts index 91fde3cd27..f2453f314f 100644 --- a/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/TechdocsMarkdownPrinter.ts @@ -66,6 +66,10 @@ export default class TechdocsMarkdownPrinter implements MarkdownPrinter { return '../'; } + pageLink(name: string) { + return `./${name}/`; + } + srcLink( { file, lineInFile }: { file: string; lineInFile: number }, text?: string, diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index fae368b58d..7aff54d04c 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -106,7 +106,10 @@ export type MarkdownPrinter = { 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, From 922c7296fd5e02093694a7893ef0d49524ab9d24 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:38:29 +0200 Subject: [PATCH 26/54] docgen: use github-slugger to generate better slugs --- packages/docgen/package.json | 6 ++++-- .../docgen/src/docgen/GitHubMarkdownPrinter.ts | 3 ++- yarn.lock | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/docgen/package.json b/packages/docgen/package.json index eafddbdda3..6e73dfeda5 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -29,11 +29,13 @@ "chalk": "^4.0.0", "commander": "^4.1.1", "fs-extra": "^9.0.0", - "typescript": "^3.9.3", - "ts-node": "^8.6.2" + "github-slugger": "^1.3.0", + "ts-node": "^8.6.2", + "typescript": "^3.9.3" }, "devDependencies": { "@types/fs-extra": "^9.0.1", + "@types/github-slugger": "^1.3.0", "@types/node": "^13.7.2", "nodemon": "^2.0.2" }, diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts index fd82e023d3..64d38d8b15 100644 --- a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import GithubSlugger from 'github-slugger'; import sortSelector from './sortSelector'; import { MarkdownPrinter, TypeLink } from './types'; import { execSync } from 'child_process'; @@ -46,7 +47,7 @@ export default class GithubMarkdownPrinter implements MarkdownPrinter { } headerLink(heading: string): string { - const slug = heading.replace(/[^a-z0-9]/gi, '-').toLowerCase(); + const slug = GithubSlugger.slug(heading); return `#${slug}`; } diff --git a/yarn.lock b/yarn.lock index 6c0ff99388..75746071c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3545,6 +3545,11 @@ resolved "https://registry.npmjs.org/@types/git-url-parse/-/git-url-parse-9.0.0.tgz#aac1315a44fa4ed5a52c3820f6c3c2fb79cbd12d" integrity sha512-kA2RxBT/r/ZuDDKwMl+vFWn1Z0lfm1/Ik6Qb91wnSzyzCDa/fkM8gIOq6ruB7xfr37n6Mj5dyivileUVKsidlg== +"@types/github-slugger@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz#16ab393b30d8ae2a111ac748a015ac05a1fc5524" + integrity sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g== + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -7889,6 +7894,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +"emoji-regex@>=6.0.0 <=6.1.1": + version "6.1.1" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" + integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= + emoji-regex@^7.0.1, emoji-regex@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -9435,6 +9445,13 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +github-slugger@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" + integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== + dependencies: + emoji-regex ">=6.0.0 <=6.1.1" + glob-base@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" From 85ca747cd367aa55c560fa11994f2c7755a0c555 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:42:41 +0200 Subject: [PATCH 27/54] docgen: run prettier on generated docs --- packages/docgen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 6e73dfeda5..f2675eef24 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -18,7 +18,7 @@ "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", + "docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github && prettier --write ../../docs/reference/utility-apis", "clean": "backstage-cli clean", "start": "nodemon --" }, From c75beb460df9b051bfebf1d12c5278b78780f8ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Jul 2020 10:43:17 +0200 Subject: [PATCH 28/54] docs: regenerate api reference docs --- docs/reference/utility-apis/AlertApi.md | 17 ++-- docs/reference/utility-apis/AppThemeApi.md | 20 ++--- .../utility-apis/BackstageIdentityApi.md | 14 ++-- docs/reference/utility-apis/Config.md | 12 +-- docs/reference/utility-apis/ErrorApi.md | 21 +++-- .../reference/utility-apis/FeatureFlagsApi.md | 4 +- docs/reference/utility-apis/IdentityApi.md | 6 +- docs/reference/utility-apis/OAuthApi.md | 16 ++-- .../reference/utility-apis/OAuthRequestApi.md | 22 ++--- .../utility-apis/OpenIdConnectApi.md | 10 +-- docs/reference/utility-apis/ProfileInfoApi.md | 16 ++-- docs/reference/utility-apis/README.md | 80 ++++++++++--------- .../reference/utility-apis/SessionStateApi.md | 24 +++--- docs/reference/utility-apis/StorageApi.md | 18 ++--- 14 files changed, 141 insertions(+), 139 deletions(-) diff --git a/docs/reference/utility-apis/AlertApi.md b/docs/reference/utility-apis/AlertApi.md index b7d1a4d65d..1f0db84e29 100644 --- a/docs/reference/utility-apis/AlertApi.md +++ b/docs/reference/utility-apis/AlertApi.md @@ -1,10 +1,9 @@ # AlertApi The AlertApi type is defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L29). +[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L29). -The following Utility API implements this type: -[alertApiRef](./README.md#alertapiref) +The following Utility API implements this type: [alertApiRef](./README.md#alert) ## Members @@ -39,9 +38,9 @@ export type AlertMessage = { Defined at -[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L19). +[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L19). -Referenced by: [post](#post), [alert\$](#alert-). +Referenced by: [post](#post), [alert\$](#alert). ### Observable @@ -68,9 +67,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [alert\$](#alert-). +Referenced by: [alert\$](#alert). ### Observer @@ -87,7 +86,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -110,6 +109,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md index 79d41ef6f6..02ab83afff 100644 --- a/docs/reference/utility-apis/AppThemeApi.md +++ b/docs/reference/utility-apis/AppThemeApi.md @@ -1,10 +1,10 @@ # AppThemeApi The AppThemeApi type is defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50). The following Utility API implements this type: -[appThemeApiRef](./README.md#appthemeapiref) +[appThemeApiRef](./README.md#apptheme) ## Members @@ -76,7 +76,7 @@ export type AppTheme = { Defined at -[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24). +[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24). Referenced by: [getInstalledThemes](#getinstalledthemes). @@ -87,7 +87,7 @@ export type BackstagePalette = Palette & Palette Defined at -[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L63). +[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L63). Referenced by: [BackstageTheme](#backstagetheme). @@ -100,7 +100,7 @@ export interface BackstageTheme extends Theme { Defined at -[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L66). +[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L66). Referenced by: [AppTheme](#apptheme). @@ -129,9 +129,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [activeThemeId\$](#activethemeid-). +Referenced by: [activeThemeId\$](#activethemeid). ### Observer @@ -148,7 +148,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -197,7 +197,7 @@ type PaletteAdditions = { Defined at -[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/theme/src/types.ts#L23). +[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L23). Referenced by: [BackstagePalette](#backstagepalette). @@ -220,6 +220,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md index 717a294621..a9e0203403 100644 --- a/docs/reference/utility-apis/BackstageIdentityApi.md +++ b/docs/reference/utility-apis/BackstageIdentityApi.md @@ -1,17 +1,17 @@ # BackstageIdentityApi The BackstageIdentityApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L144). +[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L144). The following Utility APIs implement this type: -- [githubAuthApiRef](./README.md#githubauthapiref) +- [githubAuthApiRef](./README.md#githubauth) -- [gitlabAuthApiRef](./README.md#gitlabauthapiref) +- [gitlabAuthApiRef](./README.md#gitlabauth) -- [googleAuthApiRef](./README.md#googleauthapiref) +- [googleAuthApiRef](./README.md#googleauth) -- [oktaAuthApiRef](./README.md#oktaauthapiref) +- [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -62,7 +62,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getBackstageIdentity](#getbackstageidentity). @@ -83,6 +83,6 @@ export type BackstageIdentity = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L157). +[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L157). Referenced by: [getBackstageIdentity](#getbackstageidentity). diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md index ef53c5adcf..bca590bc9b 100644 --- a/docs/reference/utility-apis/Config.md +++ b/docs/reference/utility-apis/Config.md @@ -1,10 +1,10 @@ # Config The Config type is defined at -[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32). The following Utility API implements this type: -[configApiRef](./README.md#configapiref) +[configApiRef](./README.md#config) ## Members @@ -132,7 +132,7 @@ export type Config = { Defined at -[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L32). +[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32). Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig), [getConfigArray](#getconfigarray), @@ -145,7 +145,7 @@ export type JsonArray = JsonValue[] Defined at -[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L18). +[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L18). Referenced by: [JsonValue](#jsonvalue). @@ -156,7 +156,7 @@ export type JsonObject = { [key in string]?: JsonValue Defined at -[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L17). +[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L17). Referenced by: [JsonValue](#jsonvalue). @@ -173,7 +173,7 @@ export type JsonValue = Defined at -[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/config/src/types.ts#L19). +[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L19). Referenced by: [get](#get), [getOptional](#getoptional), [JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config). diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md index efd4a3ed00..10ae47e6df 100644 --- a/docs/reference/utility-apis/ErrorApi.md +++ b/docs/reference/utility-apis/ErrorApi.md @@ -1,10 +1,9 @@ # ErrorApi The ErrorApi type is defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). +[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L53). -The following Utility API implements this type: -[errorApiRef](./README.md#errorapiref) +The following Utility API implements this type: [errorApiRef](./README.md#error) ## Members @@ -42,9 +41,9 @@ type Error = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). +[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L24). -Referenced by: [post](#post), [error\$](#error-). +Referenced by: [post](#post), [error\$](#error). ### ErrorContext @@ -59,9 +58,9 @@ export type ErrorContext = { Defined at -[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). +[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L33). -Referenced by: [post](#post), [error\$](#error-). +Referenced by: [post](#post), [error\$](#error). ### Observable @@ -88,9 +87,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [error\$](#error-). +Referenced by: [error\$](#error). ### Observer @@ -107,7 +106,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -130,6 +129,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md index b3320b404a..5069c9272c 100644 --- a/docs/reference/utility-apis/FeatureFlagsApi.md +++ b/docs/reference/utility-apis/FeatureFlagsApi.md @@ -1,10 +1,10 @@ # FeatureFlagsApi The FeatureFlagsApi type is defined at -[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41). +[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41). The following Utility API implements this type: -[featureFlagsApiRef](./README.md#featureflagsapiref) +[featureFlagsApiRef](./README.md#featureflags) ## Members diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md index f5de414cf5..2c94b7fcd0 100644 --- a/docs/reference/utility-apis/IdentityApi.md +++ b/docs/reference/utility-apis/IdentityApi.md @@ -1,10 +1,10 @@ # IdentityApi The IdentityApi type is defined at -[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). +[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L22). The following Utility API implements this type: -[identityApiRef](./README.md#identityapiref) +[identityApiRef](./README.md#identity) ## Members @@ -76,6 +76,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L172). +[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md index e3334601f1..09a4c4b1b4 100644 --- a/docs/reference/utility-apis/OAuthApi.md +++ b/docs/reference/utility-apis/OAuthApi.md @@ -1,19 +1,19 @@ # OAuthApi The OAuthApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L67). +[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L67). The following Utility APIs implement this type: -- [githubAuthApiRef](./README.md#githubauthapiref) +- [githubAuthApiRef](./README.md#githubauth) -- [gitlabAuthApiRef](./README.md#gitlabauthapiref) +- [gitlabAuthApiRef](./README.md#gitlabauth) -- [googleAuthApiRef](./README.md#googleauthapiref) +- [googleAuthApiRef](./README.md#googleauth) -- [oauth2ApiRef](./README.md#oauth2apiref) +- [oauth2ApiRef](./README.md#oauth2) -- [oktaAuthApiRef](./README.md#oktaauthapiref) +- [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -88,7 +88,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getAccessToken](#getaccesstoken). @@ -114,6 +114,6 @@ export type OAuthScope = string | string[] Defined at -[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L38). +[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L38). Referenced by: [getAccessToken](#getaccesstoken). diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md index 895c515fb0..fc38ca827b 100644 --- a/docs/reference/utility-apis/OAuthRequestApi.md +++ b/docs/reference/utility-apis/OAuthRequestApi.md @@ -1,10 +1,10 @@ # OAuthRequestApi The OAuthRequestApi type is defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99). The following Utility API implements this type: -[oauthRequestApiRef](./README.md#oauthrequestapiref) +[oauthRequestApiRef](./README.md#oauthrequest) ## Members @@ -72,7 +72,7 @@ export type AuthProvider = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27). Referenced by: [AuthRequesterOptions](#authrequesteroptions), [PendingAuthRequest](#pendingauthrequest). @@ -96,7 +96,7 @@ export type AuthRequester<AuthResponse> = ( Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66). Referenced by: [createAuthRequester](#createauthrequester). @@ -121,7 +121,7 @@ export type AuthRequesterOptions<AuthResponse> = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43). Referenced by: [createAuthRequester](#createauthrequester). @@ -150,9 +150,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [authRequest\$](#authrequest-). +Referenced by: [authRequest\$](#authrequest). ### Observer @@ -169,7 +169,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -204,9 +204,9 @@ export type PendingAuthRequest = { Defined at -[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). +[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77). -Referenced by: [authRequest\$](#authrequest-). +Referenced by: [authRequest\$](#authrequest). ### Subscription @@ -227,6 +227,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md index c626ccf3a1..ecbf439b84 100644 --- a/docs/reference/utility-apis/OpenIdConnectApi.md +++ b/docs/reference/utility-apis/OpenIdConnectApi.md @@ -1,15 +1,15 @@ # OpenIdConnectApi The OpenIdConnectApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L104). +[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L104). The following Utility APIs implement this type: -- [googleAuthApiRef](./README.md#googleauthapiref) +- [googleAuthApiRef](./README.md#googleauth) -- [oauth2ApiRef](./README.md#oauth2apiref) +- [oauth2ApiRef](./README.md#oauth2) -- [oktaAuthApiRef](./README.md#oktaauthapiref) +- [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -70,6 +70,6 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getIdToken](#getidtoken). diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md index 6fcbc99950..469c923cb0 100644 --- a/docs/reference/utility-apis/ProfileInfoApi.md +++ b/docs/reference/utility-apis/ProfileInfoApi.md @@ -1,19 +1,19 @@ # ProfileInfoApi The ProfileInfoApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L127). +[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L127). The following Utility APIs implement this type: -- [githubAuthApiRef](./README.md#githubauthapiref) +- [githubAuthApiRef](./README.md#githubauth) -- [gitlabAuthApiRef](./README.md#gitlabauthapiref) +- [gitlabAuthApiRef](./README.md#gitlabauth) -- [googleAuthApiRef](./README.md#googleauthapiref) +- [googleAuthApiRef](./README.md#googleauth) -- [oauth2ApiRef](./README.md#oauth2apiref) +- [oauth2ApiRef](./README.md#oauth2) -- [oktaAuthApiRef](./README.md#oktaauthapiref) +- [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -61,7 +61,7 @@ export type AuthRequestOptions = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L40). +[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40). Referenced by: [getProfile](#getprofile). @@ -89,6 +89,6 @@ export type ProfileInfo = { Defined at -[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L172). +[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172). Referenced by: [getProfile](#getprofile). diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md index 398d55f605..66da8403b0 100644 --- a/docs/reference/utility-apis/README.md +++ b/docs/reference/utility-apis/README.md @@ -9,127 +9,131 @@ https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md. Used to report alerts and forward them to the app -Implemented type: [AlertApi](./AlertApi) +Implemented type: [AlertApi](./AlertApi.md) ApiRef: -[alertApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AlertApi.ts#L41) +[alertApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L41) ### appTheme API Used to configure the app theme, and enumerate options -Implemented type: [AppThemeApi](./AppThemeApi) +Implemented type: [AppThemeApi](./AppThemeApi.md) ApiRef: -[appThemeApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) +[appThemeApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74) ### config Used to access runtime configuration -Implemented type: [Config](./Config) +Implemented type: [Config](./Config.md) ApiRef: -[configApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) +[configApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ConfigApi.ts#L22) ### error Used to report errors and forward them to the app -Implemented type: [ErrorApi](./ErrorApi) +Implemented type: [ErrorApi](./ErrorApi.md) ApiRef: -[errorApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) +[errorApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L65) ### featureFlags Used to toggle functionality in features across Backstage -Implemented type: [FeatureFlagsApi](./FeatureFlagsApi) +Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md) ApiRef: -[featureFlagsApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) +[featureFlagsApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58) ### githubAuth Provides authentication towards Github APIs -Implemented types: [OAuthApi](./OAuthApi), [ProfileInfoApi](./ProfileInfoApi), -[BackstageIdentityApi](./BackstageIdentityApi), -[SessionStateApi](./SessionStateApi) +Implemented types: [OAuthApi](./OAuthApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), +[SessionStateApi](./SessionStateApi.md) ApiRef: -[githubAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L230) +[githubAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L230) ### gitlabAuth Provides authentication towards Gitlab APIs -Implemented types: [OAuthApi](./OAuthApi), [ProfileInfoApi](./ProfileInfoApi), -[BackstageIdentityApi](./BackstageIdentityApi), -[SessionStateApi](./SessionStateApi) +Implemented types: [OAuthApi](./OAuthApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), +[SessionStateApi](./SessionStateApi.md) ApiRef: -[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L260) +[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L260) ### googleAuth Provides authentication towards Google APIs and identities -Implemented types: [OAuthApi](./OAuthApi), -[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), -[BackstageIdentityApi](./BackstageIdentityApi), -[SessionStateApi](./SessionStateApi) +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), +[SessionStateApi](./SessionStateApi.md) ApiRef: -[googleAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L213) +[googleAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L213) ### identity Provides access to the identity of the signed in user -Implemented type: [IdentityApi](./IdentityApi) +Implemented type: [IdentityApi](./IdentityApi.md) ApiRef: -[identityApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) +[identityApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L54) ### oauth2 Example of how to use oauth2 custom provider -Implemented types: [OAuthApi](./OAuthApi), -[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), -[SessionStateApi](./SessionStateApi) +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md) ApiRef: -[oauth2ApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L270) +[oauth2ApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L270) ### oauthRequest An API for implementing unified OAuth flows in Backstage -Implemented type: [OAuthRequestApi](./OAuthRequestApi) +Implemented type: [OAuthRequestApi](./OAuthRequestApi.md) ApiRef: -[oauthRequestApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) +[oauthRequestApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130) ### oktaAuth Provides authentication towards Okta APIs -Implemented types: [OAuthApi](./OAuthApi), -[OpenIdConnectApi](./OpenIdConnectApi), [ProfileInfoApi](./ProfileInfoApi), -[BackstageIdentityApi](./BackstageIdentityApi), -[SessionStateApi](./SessionStateApi) +Implemented types: [OAuthApi](./OAuthApi.md), +[OpenIdConnectApi](./OpenIdConnectApi.md), +[ProfileInfoApi](./ProfileInfoApi.md), +[BackstageIdentityApi](./BackstageIdentityApi.md), +[SessionStateApi](./SessionStateApi.md) ApiRef: -[oktaAuthApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L243) +[oktaAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L243) ### storage Provides the ability to store data which is unique to the user -Implemented type: [StorageApi](./StorageApi) +Implemented type: [StorageApi](./StorageApi.md) ApiRef: -[storageApiRef](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L68) +[storageApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L68) diff --git a/docs/reference/utility-apis/SessionStateApi.md b/docs/reference/utility-apis/SessionStateApi.md index d15074aa9d..7ed67cbc62 100644 --- a/docs/reference/utility-apis/SessionStateApi.md +++ b/docs/reference/utility-apis/SessionStateApi.md @@ -1,19 +1,19 @@ # SessionStateApi The SessionStateApi type is defined at -[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L201). +[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L201). The following Utility APIs implement this type: -- [githubAuthApiRef](./README.md#githubauthapiref) +- [githubAuthApiRef](./README.md#githubauth) -- [gitlabAuthApiRef](./README.md#gitlabauthapiref) +- [gitlabAuthApiRef](./README.md#gitlabauth) -- [googleAuthApiRef](./README.md#googleauthapiref) +- [googleAuthApiRef](./README.md#googleauth) -- [oauth2ApiRef](./README.md#oauth2apiref) +- [oauth2ApiRef](./README.md#oauth2) -- [oktaAuthApiRef](./README.md#oktaauthapiref) +- [oktaAuthApiRef](./README.md#oktaauth) ## Members @@ -52,9 +52,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [sessionState\$](#sessionstate-). +Referenced by: [sessionState\$](#sessionstate). ### Observer @@ -71,7 +71,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -87,9 +87,9 @@ export enum SessionState { Defined at -[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/auth.ts#L192). +[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L192). -Referenced by: [sessionState\$](#sessionstate-). +Referenced by: [sessionState\$](#sessionstate). ### Subscription @@ -110,6 +110,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md index dd366bcf18..1eadec1302 100644 --- a/docs/reference/utility-apis/StorageApi.md +++ b/docs/reference/utility-apis/StorageApi.md @@ -1,10 +1,10 @@ # StorageApi The StorageApi type is defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31). The following Utility API implements this type: -[storageApiRef](./README.md#storageapiref) +[storageApiRef](./README.md#storage) ## Members @@ -79,9 +79,9 @@ export type Observable<T> = { Defined at -[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L53). +[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53). -Referenced by: [observe\$](#observe-), [StorageApi](#storageapi). +Referenced by: [observe\$](#observe), [StorageApi](#storageapi). ### Observer @@ -98,7 +98,7 @@ export type Observer<T> = { Defined at -[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L24). +[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24). Referenced by: [Observable](#observable). @@ -144,7 +144,7 @@ export interface StorageApi { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L31). +[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31). Referenced by: [forBucket](#forbucket). @@ -158,9 +158,9 @@ export type StorageValueChange<T = any> = { Defined at -[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/apis/definitions/StorageApi.ts#L21). +[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L21). -Referenced by: [observe\$](#observe-), [StorageApi](#storageapi). +Referenced by: [observe\$](#observe), [StorageApi](#storageapi). ### Subscription @@ -181,6 +181,6 @@ export type Subscription = { Defined at -[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/4df02a253f6903e1ca20184369f5655e2d49d893/packages/core-api/src/types.ts#L33). +[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33). Referenced by: [Observable](#observable). From 9e2606a89a7a4e7b8608cf820f83f645f2329df7 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Wed, 22 Jul 2020 16:44:14 +0200 Subject: [PATCH 29/54] feature(techdocs): added an example component in techdocs-backend containing a documentation reference --- .../examples/documented-component/docs/index.md | 3 +++ .../documented-component/documented-component.yaml | 11 +++++++++++ .../examples/documented-component/mkdocs.yml | 7 +++++++ plugins/techdocs-backend/package.json | 3 ++- plugins/techdocs-backend/scripts/mock-data.sh | 12 ++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 plugins/techdocs-backend/examples/documented-component/docs/index.md create mode 100644 plugins/techdocs-backend/examples/documented-component/documented-component.yaml create mode 100644 plugins/techdocs-backend/examples/documented-component/mkdocs.yml create mode 100755 plugins/techdocs-backend/scripts/mock-data.sh diff --git a/plugins/techdocs-backend/examples/documented-component/docs/index.md b/plugins/techdocs-backend/examples/documented-component/docs/index.md new file mode 100644 index 0000000000..bf0895710f --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/docs/index.md @@ -0,0 +1,3 @@ +# example docs + +This is a basic example of documentation. diff --git a/plugins/techdocs-backend/examples/documented-component/documented-component.yaml b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml new file mode 100644 index 0000000000..64eba5598b --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: documented-component + description: A Service with TechDocs documentation + annotations: + spotify.com/techdocs-ref: 'dir:./' +spec: + type: service + lifecycle: experimental + owner: documented@example.com diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml new file mode 100644 index 0000000000..deb46069d7 --- /dev/null +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -0,0 +1,7 @@ +site_name: 'example-docs' + +nav: + - Home: index.md + +plugins: + - techdocs-core \ No newline at end of file diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 03a6a8dcb9..b66509abe5 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -17,7 +17,8 @@ "test": "backstage-cli test", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "clean": "backstage-cli clean", + "mock-data": "./scripts/mock-data.sh" }, "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.15", diff --git a/plugins/techdocs-backend/scripts/mock-data.sh b/plugins/techdocs-backend/scripts/mock-data.sh new file mode 100755 index 0000000000..0630490694 --- /dev/null +++ b/plugins/techdocs-backend/scripts/mock-data.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +for URL in \ + 'documented-component/documented-component.yaml' \ +; do \ + curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/examples/${URL}\"}" + echo +done From 1ac69ba3e36a79f5fcc0bfd1535980f7b91b52d1 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Tue, 21 Jul 2020 13:28:46 -0500 Subject: [PATCH 30/54] implement gitlabapi processor for private gitlab repos --- .../src/ingestion/LocationReaders.ts | 3 +- .../processors/GitlabApiReaderProcessor.ts | 109 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 8ec575d6ec..9c29e45ca3 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -27,6 +27,7 @@ import { EntityPolicyProcessor } from './processors/EntityPolicyProcessor'; import { FileReaderProcessor } from './processors/FileReaderProcessor'; import { GithubReaderProcessor } from './processors/GithubReaderProcessor'; import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor'; +import { GitlabApiReaderProcessor } from './processors/GitlabApiReaderProcessor'; import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; import { LocationRefProcessor } from './processors/LocationEntityProcessor'; import * as result from './processors/results'; @@ -59,6 +60,7 @@ export class LocationReaders implements LocationReader { new FileReaderProcessor(), new GithubReaderProcessor(), new GithubApiReaderProcessor(), + new GitlabApiReaderProcessor(), new GitlabReaderProcessor(), new YamlProcessor(), new EntityPolicyProcessor(entityPolicy), @@ -102,7 +104,6 @@ export class LocationReaders implements LocationReader { }); } } - if (newItems.length === 0) { return output; } diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts new file mode 100644 index 0000000000..00abaab430 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Entity, LocationSpec } from '@backstage/catalog-model'; +import fetch, { RequestInit, HeadersInit } from 'node-fetch'; +import lodash from 'lodash'; +import yaml from 'yaml'; +import * as result from './results'; +import { LocationProcessor, LocationProcessorEmit } from './types'; + +export class GitlabApiReaderProcessor implements LocationProcessor { + private privateToken: string = process.env.GITLAB_PRIVATE_TOKEN || ''; + + getRequestOptions(): RequestInit { + const headers: HeadersInit = { 'PRIVATE-TOKEN': '' }; + if (this.privateToken !== '') { + headers['PRIVATE-TOKEN'] = this.privateToken; + } + + const requestOptions: RequestInit = { + headers, + }; + + return requestOptions; + } + + async readLocation( + location: LocationSpec, + optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'gitlab/api') { + return false; + } + + try { + // const url = this.buildRawUrl(location.target); + const url = new URL(location.target); + const response = await fetch(url.toString(), this.getRequestOptions()); + if (response.ok) { + const data = await response.buffer(); + emit(result.data(location, data)); + } else { + const message = `${location.target} could not be read as ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + if (!optional) { + emit(result.notFoundError(location, message)); + } + } else { + emit(result.generalError(location, message)); + } + } + } catch (e) { + const message = `Unable to read ${location.type} ${location.target}, ${e}`; + emit(result.generalError(location, message)); + } + return true; + } + + // We need our own parseData because the gitlab api url has `/raw?ref=branch` + // on the end which doesn't match the regex in YamlProcessor (.ya?ml$) + async parseData( + data: Buffer, + location: LocationSpec, + emit: LocationProcessorEmit, + ): Promise { + if (!location.target.match(/\.ya?ml/)) { + return false; + } + + let documents: yaml.Document.Parsed[]; + try { + documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d); + } catch (e) { + emit(result.generalError(location, `Failed to parse YAML, ${e}`)); + return true; + } + + for (const document of documents) { + if (document.errors?.length) { + const message = `YAML error, ${document.errors[0]}`; + emit(result.generalError(location, message)); + } else { + const json = document.toJSON(); + if (lodash.isPlainObject(json)) { + emit(result.entity(location, json as Entity)); + } else { + const message = `Expected object at root, got ${typeof json}`; + emit(result.generalError(location, message)); + } + } + } + + return true; + } +} From a0787d59a48d67c5cdf8a457ee6996e0f59d7553 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 22 Jul 2020 16:13:42 -0500 Subject: [PATCH 31/54] lookup gitlab project ID with gitlab API and add tests --- .../src/ingestion/LocationReaders.ts | 1 + .../GitlabApiReaderProcessor.test.ts | 94 ++++++++++++++++++ .../processors/GitlabApiReaderProcessor.ts | 96 ++++++++++++------- 3 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 9c29e45ca3..545465cde0 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -104,6 +104,7 @@ export class LocationReaders implements LocationReader { }); } } + if (newItems.length === 0) { return output; } diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts new file mode 100644 index 0000000000..a429a0e6fb --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { GitlabApiReaderProcessor } from './GitlabApiReaderProcessor'; + +describe('GitlabApiReaderProcessor', () => { + it('should build raw api', () => { + const processor = new GitlabApiReaderProcessor(); + + const tests = [ + { + target: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + url: new URL( + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + ), + err: undefined, + }, + { + target: + 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + url: new URL( + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + ), + err: undefined, + }, + { + target: + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + url: new URL( + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml?ref=branch', + ), + err: undefined, + }, + { + target: + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/', + url: null, + err: + 'Incorrect url: https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/, Error: Gitlab url does not end in .ya?ml', + }, + ]; + + for (const test of tests) { + if (test.err) { + expect(() => processor.buildRawUrl(test.target, 12345)).toThrowError( + test.err, + ); + } else { + expect(processor.buildRawUrl(test.target, 12345)).toEqual(test.url); + } + } + }); + + it('should return request options', () => { + const tests = [ + { + token: '0123456789', + expect: { + headers: { + 'PRIVATE-TOKEN': '0123456789', + }, + }, + }, + { + token: '', + expect: { + headers: { + 'PRIVATE-TOKEN': '', + }, + }, + }, + ]; + + for (const test of tests) { + process.env.GITLAB_PRIVATE_TOKEN = test.token; + const processor = new GitlabApiReaderProcessor(); + expect(processor.getRequestOptions()).toEqual(test.expect); + } + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts index 00abaab430..6964640eb4 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitlabApiReaderProcessor.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/catalog-model'; import fetch, { RequestInit, HeadersInit } from 'node-fetch'; -import lodash from 'lodash'; -import yaml from 'yaml'; import * as result from './results'; import { LocationProcessor, LocationProcessorEmit } from './types'; @@ -47,8 +45,8 @@ export class GitlabApiReaderProcessor implements LocationProcessor { } try { - // const url = this.buildRawUrl(location.target); - const url = new URL(location.target); + const projectID = await this.getProjectID(location.target); + const url = this.buildRawUrl(location.target, projectID); const response = await fetch(url.toString(), this.getRequestOptions()); if (response.ok) { const data = await response.buffer(); @@ -70,40 +68,66 @@ export class GitlabApiReaderProcessor implements LocationProcessor { return true; } - // We need our own parseData because the gitlab api url has `/raw?ref=branch` - // on the end which doesn't match the regex in YamlProcessor (.ya?ml$) - async parseData( - data: Buffer, - location: LocationSpec, - emit: LocationProcessorEmit, - ): Promise { - if (!location.target.match(/\.ya?ml/)) { - return false; - } - - let documents: yaml.Document.Parsed[]; + // convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + // to https://gitlab.com/api/v4/projects//repository/files/filepath?ref=branch + buildRawUrl(target: string, projectID: Number): URL { try { - documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d); - } catch (e) { - emit(result.generalError(location, `Failed to parse YAML, ${e}`)); - return true; - } + const url = new URL(target); - for (const document of documents) { - if (document.errors?.length) { - const message = `YAML error, ${document.errors[0]}`; - emit(result.generalError(location, message)); - } else { - const json = document.toJSON(); - if (lodash.isPlainObject(json)) { - emit(result.entity(location, json as Entity)); - } else { - const message = `Expected object at root, got ${typeof json}`; - emit(result.generalError(location, message)); - } + const branchAndfilePath = url.pathname.split('/-/blob/')[1]; + + if (!branchAndfilePath.match(/\.ya?ml$/)) { + throw new Error('Gitlab url does not end in .ya?ml'); } - } - return true; + const [branch, ...filePath] = branchAndfilePath.split('/'); + + url.pathname = [ + '/api/v4/projects', + projectID, + 'repository/files', + encodeURIComponent(filePath.join('/')), + 'raw', + ].join('/'); + url.search = `?ref=${branch}`; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } + + async getProjectID(target: string): Promise { + const url = new URL(target); + + if ( + // absPaths to gitlab files should contain /-/blob + // ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + !url.pathname.match(/\/\-\/blob\//) + ) { + throw new Error('Please provide full path to yaml file from Gitlab'); + } + try { + const repo = url.pathname.split('/-/blob/')[0]; + + // Find ProjectID from url + // convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath' + // to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo' + const repoIDLookup = new URL( + `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent( + repo.replace(/^\//, ''), + )}`, + ); + const response = await fetch( + repoIDLookup.toString(), + this.getRequestOptions(), + ); + const projectIDJson = await response.json(); + const projectID: Number = projectIDJson.id; + + return projectID; + } catch (e) { + throw new Error(`Could not get Gitlab ProjectID for: ${target}, ${e}`); + } } } From 851b040c41f65d58223fafa02fbfea749cf00aa4 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Thu, 23 Jul 2020 14:14:30 +0200 Subject: [PATCH 32/54] changed spotify.com namespace to backstage.io --- .../examples/documented-component/documented-component.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/examples/documented-component/documented-component.yaml b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml index 64eba5598b..d5a1214b15 100644 --- a/plugins/techdocs-backend/examples/documented-component/documented-component.yaml +++ b/plugins/techdocs-backend/examples/documented-component/documented-component.yaml @@ -4,7 +4,7 @@ metadata: name: documented-component description: A Service with TechDocs documentation annotations: - spotify.com/techdocs-ref: 'dir:./' + backstage.io/techdocs-ref: 'dir:./' spec: type: service lifecycle: experimental From 7808c115d1162912ceda0bcff6b8f09ffbd78ad4 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Thu, 23 Jul 2020 14:28:50 +0200 Subject: [PATCH 33/54] Added empty line at the end of file --- .../techdocs-backend/examples/documented-component/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml index deb46069d7..6a3f85d020 100644 --- a/plugins/techdocs-backend/examples/documented-component/mkdocs.yml +++ b/plugins/techdocs-backend/examples/documented-component/mkdocs.yml @@ -4,4 +4,4 @@ nav: - Home: index.md plugins: - - techdocs-core \ No newline at end of file + - techdocs-core From 2e605f91729b3260bd4f59eda7a3fcb5640f2e68 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 23 Jul 2020 10:49:40 +0200 Subject: [PATCH 34/54] feat: add UrlReaderProcessor to consume entities from a simple URL --- plugins/catalog-backend/package.json | 1 + .../src/ingestion/LocationReaders.ts | 2 + .../processors/UrlReaderProcessor.test.ts | 80 +++++++++++++++++++ .../processors/UrlReaderProcessor.ts | 55 +++++++++++++ plugins/catalog-backend/src/setupTests.ts | 2 - yarn.lock | 2 +- 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9c7a18087d..b342cae3db 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -46,6 +46,7 @@ "@types/uuid": "^8.0.0", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", + "msw": "^0.19.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 8ec575d6ec..2aa41f6c50 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -28,6 +28,7 @@ import { FileReaderProcessor } from './processors/FileReaderProcessor'; import { GithubReaderProcessor } from './processors/GithubReaderProcessor'; import { GithubApiReaderProcessor } from './processors/GithubApiReaderProcessor'; import { GitlabReaderProcessor } from './processors/GitlabReaderProcessor'; +import { UrlReaderProcessor } from './processors/UrlReaderProcessor'; import { LocationRefProcessor } from './processors/LocationEntityProcessor'; import * as result from './processors/results'; import { @@ -60,6 +61,7 @@ export class LocationReaders implements LocationReader { new GithubReaderProcessor(), new GithubApiReaderProcessor(), new GitlabReaderProcessor(), + new UrlReaderProcessor(), new YamlProcessor(), new EntityPolicyProcessor(entityPolicy), new LocationRefProcessor(), diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts new file mode 100644 index 0000000000..e30b320bf4 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { UrlReaderProcessor } from './UrlReaderProcessor'; +import { + LocationProcessorDataResult, + LocationProcessorResult, + LocationProcessorErrorResult, +} from './types'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +describe('UrlReaderProcessor', () => { + const mockApiOrigin = 'http://localhost:23000'; + const server = setupServer(); + + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + it('should load from url', async () => { + const processor = new UrlReaderProcessor(); + const spec = { + type: 'url', + target: `${mockApiOrigin}/component.yaml`, + }; + + server.use( + rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) => + res(ctx.body('Hello')), + ), + ); + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as LocationProcessorDataResult; + + expect(generated.type).toBe('data'); + expect(generated.location).toBe(spec); + expect(generated.data.toString('utf8')).toBe('Hello'); + }); + + it('should fail load from url with error', async () => { + const processor = new UrlReaderProcessor(); + const spec = { + type: 'url', + target: `${mockApiOrigin}/component-notfound.yaml`, + }; + + server.use( + rest.get(`${mockApiOrigin}/component-notfound.yaml`, (_, res, ctx) => { + return res(ctx.status(404)); + }), + ); + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as LocationProcessorErrorResult; + + expect(generated.type).toBe('error'); + expect(generated.location).toBe(spec); + expect(generated.error.name).toBe('NotFoundError'); + expect(generated.error.message).toBe( + `${mockApiOrigin}/component-notfound.yaml could not be read, 404 Not Found`, + ); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts new file mode 100644 index 0000000000..0c879ea70c --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { LocationSpec } from '@backstage/catalog-model'; +import fetch from 'node-fetch'; +import * as result from './results'; +import { LocationProcessor, LocationProcessorEmit } from './types'; + +export class UrlReaderProcessor implements LocationProcessor { + async readLocation( + location: LocationSpec, + optional: boolean, + emit: LocationProcessorEmit, + ): Promise { + if (location.type !== 'url') { + return false; + } + + try { + const response = await fetch(location.target); + + if (response.ok) { + const data = await response.buffer(); + emit(result.data(location, data)); + } else { + const message = `${location.target} could not be read, ${response.status} ${response.statusText}`; + if (response.status === 404) { + if (!optional) { + emit(result.notFoundError(location, message)); + } + } else { + emit(result.generalError(location, message)); + } + } + } catch (e) { + const message = `Unable to read ${location.type} ${location.target}, ${e}`; + emit(result.generalError(location, message)); + } + + return true; + } +} diff --git a/plugins/catalog-backend/src/setupTests.ts b/plugins/catalog-backend/src/setupTests.ts index f7b6ca962d..ba33cf996b 100644 --- a/plugins/catalog-backend/src/setupTests.ts +++ b/plugins/catalog-backend/src/setupTests.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -require('jest-fetch-mock').enableMocks(); - export {}; diff --git a/yarn.lock b/yarn.lock index 6c0ff99388..ae7f5feabf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13127,7 +13127,7 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.19.0: +msw@^0.19.0, msw@^0.19.5: version "0.19.5" resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f" integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q== From 2893daf3b4b2aa5cb92e73610105d509c9381499 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 23 Jul 2020 20:51:57 +0200 Subject: [PATCH 35/54] feat(cli): create-app creates backend as well with PG support --- .../cli/src/commands/create-app/createApp.ts | 11 +- .../cli/templates/default-app/app-config.yaml | 16 +++ .../default-app/packages/backend/.eslintrc.js | 3 + .../default-app/packages/backend/Dockerfile | 9 ++ .../default-app/packages/backend/README.md | 70 ++++++++++++ .../packages/backend/package.json.hbs | 51 +++++++++ .../packages/backend/src/index.test.ts | 24 ++++ .../packages/backend/src/index.ts.hbs | 105 ++++++++++++++++++ .../packages/backend/src/plugins/auth.ts | 26 +++++ .../packages/backend/src/plugins/catalog.ts | 56 ++++++++++ .../packages/backend/src/plugins/identity.ts | 22 ++++ .../packages/backend/src/plugins/proxy.ts | 25 +++++ .../backend/src/plugins/scaffolder.ts | 56 ++++++++++ .../packages/backend/src/plugins/techdocs.ts | 22 ++++ .../default-app/packages/backend/src/types.ts | 25 +++++ .../cli/templates/default-app/tsconfig.json | 12 +- 16 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 packages/cli/templates/default-app/packages/backend/.eslintrc.js create mode 100644 packages/cli/templates/default-app/packages/backend/Dockerfile create mode 100644 packages/cli/templates/default-app/packages/backend/README.md create mode 100644 packages/cli/templates/default-app/packages/backend/package.json.hbs create mode 100644 packages/cli/templates/default-app/packages/backend/src/index.test.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/index.ts.hbs create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/auth.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/catalog.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/identity.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/proxy.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/scaffolder.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/plugins/techdocs.ts create mode 100644 packages/cli/templates/default-app/packages/backend/src/types.ts diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index 4f4134957d..d0fc5e4450 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { promisify } from 'util'; import chalk from 'chalk'; import { Command } from 'commander'; -import inquirer, { Answers, Question } from 'inquirer'; +import inquirer, { Answers, Question, ChoiceBase, ui } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import os from 'os'; @@ -104,8 +104,17 @@ export default async (cmd: Command): Promise => { return true; }, }, + { + type: 'list', + name: 'dbType', + message: chalk.blue('Select database for the backend [required]'), + // @ts-ignore + choices: ['PostgreSQL', 'SQLite'], + }, ]; const answers: Answers = await inquirer.prompt(questions); + answers['dbTypePG'] = answers.dbType === 'PostgreSQL'; + answers['dbTypeSqlite'] = answers.dbType === 'SQLite'; const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); diff --git a/packages/cli/templates/default-app/app-config.yaml b/packages/cli/templates/default-app/app-config.yaml index 58f7b795ff..0d51cf88ef 100644 --- a/packages/cli/templates/default-app/app-config.yaml +++ b/packages/cli/templates/default-app/app-config.yaml @@ -4,3 +4,19 @@ app: organization: name: Acme Corporation + +backend: + baseUrl: http://localhost:7000 + listen: 0.0.0.0:7000 + cors: + origin: http://localhost:3000 + methods: [GET, POST, PUT, DELETE] + credentials: true + +proxy: + '/test': + target: 'https://example.com' + changeOrigin: true + +techdocs: + storageUrl: https://techdocs-mock-sites.storage.googleapis.com diff --git a/packages/cli/templates/default-app/packages/backend/.eslintrc.js b/packages/cli/templates/default-app/packages/backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/cli/templates/default-app/packages/backend/Dockerfile b/packages/cli/templates/default-app/packages/backend/Dockerfile new file mode 100644 index 0000000000..3e8ba36cec --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM node:12 + +WORKDIR /usr/src/app + +COPY . . + +RUN yarn install --frozen-lockfile --production + +CMD ["node", "packages/backend"] diff --git a/packages/cli/templates/default-app/packages/backend/README.md b/packages/cli/templates/default-app/packages/backend/README.md new file mode 100644 index 0000000000..90c70912f0 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/README.md @@ -0,0 +1,70 @@ +# example-backend + +This package is an EXAMPLE of a Backstage backend. + +The main purpose of this package is to provide a test bed for Backstage plugins +that have a backend part. Feel free to experiment locally or within your fork +by adding dependencies and routes to this backend, to try things out. + +Our goal is to eventually amend the create-app flow of the CLI, such that a +production ready version of a backend skeleton is made alongside the frontend +app. Until then, feel free to experiment here! + +## Development + +To run the example backend, first go to the project root and run + +```bash +yarn install +yarn tsc +yarn build +``` + +You should only need to do this once. + +After that, go to the `packages/backend` directory and run + +```bash +AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ +AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ +AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ +AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ +ROLLBAR_ACCOUNT_TOKEN=x \ +SENTRY_TOKEN=x \ +LOG_LEVEL=debug \ +yarn start +``` + +Substitute `x` for actual values, or leave them as +dummy values just to try out the backend without using the auth or sentry features. + +The backend starts up on port 7000 per default. + +## Populating The Catalog + +If you want to use the catalog functionality, you need to add so called locations +to the backend. These are places where the backend can find some entity descriptor +data to consume and serve. + +To get started, you can issue the following after starting the backend, from inside +the `plugins/catalog-backend` directory: + +```bash +yarn mock-data +``` + +You should then start seeing data on `localhost:7000/catalog/entities`. + +The catalog currently runs in-memory only, so feel free to try it out, but it will +need to be re-populated on next startup. + +## Authentication + +We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). + +Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md) + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/cli/templates/default-app/packages/backend/package.json.hbs b/packages/cli/templates/default-app/packages/backend/package.json.hbs new file mode 100644 index 0000000000..93c076a967 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/package.json.hbs @@ -0,0 +1,51 @@ +{ + "name": "backend", + "version": "0.0.0", + "main": "dist/index.cjs.js", + "types": "src/index.ts", + "private": true, + "engines": { + "node": "12" + }, + "scripts": { + "build": "backstage-cli backend:build", + "build-image": "backstage-cli backend:build-image example-backend", + "start": "backstage-cli backend:dev", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean", + "migrate:create": "knex migrate:make -x ts" + }, + "dependencies": { + "@backstage/backend-common": "^{{version}}", + "@backstage/catalog-model": "^{{version}}", + "@backstage/config": "^0.1.1-alpha.13", + "@backstage/config-loader": "^0.1.1-alpha.13", + "@backstage/plugin-auth-backend": "^{{version}}", + "@backstage/plugin-catalog-backend": "^{{version}}", + "@backstage/plugin-identity-backend": "^{{version}}", + "@backstage/plugin-proxy-backend": "^{{version}}", + "@backstage/plugin-rollbar-backend": "^{{version}}", + "@backstage/plugin-scaffolder-backend": "^{{version}}", + "@backstage/plugin-sentry-backend": "^{{version}}", + "@backstage/plugin-techdocs-backend": "^{{version}}", + "@octokit/rest": "^18.0.0", + "dockerode": "^3.2.0", + "express": "^4.17.1", + "knex": "^0.21.1", + {{#if dbTypePG}} + "pg": "^8.3.0", + {{/if}} + {{#if dbTypeSqlite}} + "sqlite3": "^4.2.0", + {{/if}} + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.15", + "@types/dockerode": "^2.5.32", + "@types/express": "^4.17.6", + "@types/express-serve-static-core": "^4.17.5", + "@types/helmet": "^0.0.47" + } +} diff --git a/packages/cli/templates/default-app/packages/backend/src/index.test.ts b/packages/cli/templates/default-app/packages/backend/src/index.test.ts new file mode 100644 index 0000000000..d18873c1f0 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/index.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { PluginEnvironment } from './types'; + +describe('test', () => { + it('unbreaks the test runner', () => { + const unbreaker = {} as PluginEnvironment; + expect(unbreaker).toBeTruthy(); + }); +}); diff --git a/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs new file mode 100644 index 0000000000..d1cf3d1ada --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +/* + * Hi! + * + * Note that this is an EXAMPLE Backstage backend. Please check the README. + * + * Happy hacking! + */ + +import { + createServiceBuilder, + getRootLogger, + useHotMemoize, +} from '@backstage/backend-common'; +import { ConfigReader, AppConfig } from '@backstage/config'; +import { loadConfig } from '@backstage/config-loader'; +import knex from 'knex'; +import auth from './plugins/auth'; +import catalog from './plugins/catalog'; +import identity from './plugins/identity'; +import scaffolder from './plugins/scaffolder'; +import proxy from './plugins/proxy'; +import techdocs from './plugins/techdocs'; +import { PluginEnvironment } from './types'; + +function makeCreateEnv(loadedConfigs: AppConfig[]) { + const config = ConfigReader.fromConfigs(loadedConfigs); + + return (plugin: string): PluginEnvironment => { + const logger = getRootLogger().child({ type: 'plugin', plugin }); + + {{#if dbTypePG}} + const knexConfig = { + client: 'pg', + useNullAsDefault: true, + connection: { + host: process.env.PG_HOST, + user: process.env.PG_USER, + password: process.env.PG_PASSWORD, + database: `backstage_plugin_${plugin}`, + }, + }; + {{/if}} + {{#if dbTypeSqlite}} + const knexConfig = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }; + {{/if}} + const database = knex(knexConfig); + database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + return { logger, database, config }; + }; +} + +async function main() { + const configs = await loadConfig(); + const configReader = ConfigReader.fromConfigs(configs); + const createEnv = makeCreateEnv(configs); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); + const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); + const authEnv = useHotMemoize(module, () => createEnv('auth')); + const identityEnv = useHotMemoize(module, () => createEnv('identity')); + const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); + const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + + const service = createServiceBuilder(module) + .loadConfig(configReader) + .addRouter('/catalog', await catalog(catalogEnv)) + .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) + .addRouter('/auth', await auth(authEnv)) + .addRouter('/identity', await identity(identityEnv)) + .addRouter('/techdocs', await techdocs(techdocsEnv)) + .addRouter('/proxy', await proxy(proxyEnv)); + + await service.start().catch(err => { + console.log(err); + process.exit(1); + }); +} + +module.hot?.accept(); +main().catch(error => { + console.error(`Backend failed to start up, ${error}`); + process.exit(1); +}); diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/auth.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/auth.ts new file mode 100644 index 0000000000..b24dd6ca6b --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/auth.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from '@backstage/plugin-auth-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + database, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config, database }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/catalog.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/catalog.ts new file mode 100644 index 0000000000..41e4226001 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/catalog.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + createRouter, + DatabaseEntitiesCatalog, + DatabaseLocationsCatalog, + DatabaseManager, + HigherOrderOperations, + LocationReaders, + runPeriodically, +} from '@backstage/plugin-catalog-backend'; +import { PluginEnvironment } from '../types'; +import { useHotCleanup } from '@backstage/backend-common'; + +export default async function createPlugin({ + logger, + database, +}: PluginEnvironment) { + const locationReader = new LocationReaders(logger); + + const db = await DatabaseManager.createDatabase(database, { logger }); + const entitiesCatalog = new DatabaseEntitiesCatalog(db); + const locationsCatalog = new DatabaseLocationsCatalog(db); + const higherOrderOperation = new HigherOrderOperations( + entitiesCatalog, + locationsCatalog, + locationReader, + logger, + ); + + useHotCleanup( + module, + runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000), + ); + + return await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger, + }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/identity.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/identity.ts new file mode 100644 index 0000000000..63a326965c --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/identity.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from '@backstage/plugin-identity-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + return await createRouter({ logger }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/proxy.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/proxy.ts new file mode 100644 index 0000000000..4964de130e --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/proxy.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ +// @ts-ignore +import { createRouter } from '@backstage/plugin-proxy-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/scaffolder.ts new file mode 100644 index 0000000000..a9e5f185ec --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + CookieCutter, + createRouter, + FilePreparer, + GithubPreparer, + Preparers, + GithubPublisher, + CreateReactAppTemplater, + Templaters, +} from '@backstage/plugin-scaffolder-backend'; +import { Octokit } from '@octokit/rest'; +import type { PluginEnvironment } from '../types'; +import Docker from 'dockerode'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + const cookiecutterTemplater = new CookieCutter(); + const craTemplater = new CreateReactAppTemplater(); + const templaters = new Templaters(); + templaters.register('cookiecutter', cookiecutterTemplater); + templaters.register('cra', craTemplater); + + const filePreparer = new FilePreparer(); + const githubPreparer = new GithubPreparer(); + const preparers = new Preparers(); + + preparers.register('file', filePreparer); + preparers.register('github', githubPreparer); + + const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); + const publisher = new GithubPublisher({ client: githubClient }); + + const dockerClient = new Docker(); + return await createRouter({ + preparers, + templaters, + publisher, + logger, + dockerClient, + }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/plugins/techdocs.ts b/packages/cli/templates/default-app/packages/backend/src/plugins/techdocs.ts new file mode 100644 index 0000000000..0d03bfabab --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/plugins/techdocs.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from '@backstage/plugin-techdocs-backend'; +import { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + return await createRouter({ logger }); +} diff --git a/packages/cli/templates/default-app/packages/backend/src/types.ts b/packages/cli/templates/default-app/packages/backend/src/types.ts new file mode 100644 index 0000000000..f7df3d05c6 --- /dev/null +++ b/packages/cli/templates/default-app/packages/backend/src/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 Knex from 'knex'; +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; + +export type PluginEnvironment = { + logger: Logger; + database: Knex; + config: Config; +}; diff --git a/packages/cli/templates/default-app/tsconfig.json b/packages/cli/templates/default-app/tsconfig.json index 52cdd1e825..9dbc3bbe27 100644 --- a/packages/cli/templates/default-app/tsconfig.json +++ b/packages/cli/templates/default-app/tsconfig.json @@ -1,8 +1,14 @@ { "extends": "@backstage/cli/config/tsconfig.json", - "include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"], - "exclude": ["**/node_modules"], + "include": [ + "packages/*/src", + "plugins/*/src", + "plugins/*/dev", + "plugins/*/migrations" + ], + "exclude": ["node_modules"], "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "skipLibCheck": true } } From 189e1bf547693b2160ad5b1191660392a2677618 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 23 Jul 2020 22:11:25 +0200 Subject: [PATCH 36/54] feat(backend): add support for PG --- packages/backend/package.json | 1 + packages/backend/src/index.ts | 34 +++++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 86d348d2a1..b040c12554 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -34,6 +34,7 @@ "dockerode": "^3.2.0", "express": "^4.17.1", "knex": "^0.21.1", + "pg": "^8.3.0", "sqlite3": "^4.2.0", "winston": "^3.2.1" }, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d060791f98..8b9edec327 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -45,11 +45,35 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { return (plugin: string): PluginEnvironment => { const logger = getRootLogger().child({ type: 'plugin', plugin }); - const database = knex({ - client: 'sqlite3', - connection: ':memory:', - useNullAsDefault: true, - }); + // Supported DBs are sqlite and postgres + const isPg = [ + 'POSTGRES_USER', + 'POSTGRES_HOST', + 'POSTGRES_PASSWORD', + ].every(key => Object.keys(process.env).includes(key)); + + let knexConfig; + + if (isPg) { + knexConfig = { + client: 'pg', + useNullAsDefault: true, + connection: { + host: process.env.PG_HOST, + user: process.env.PG_USER, + password: process.env.PG_PASSWORD, + database: `backstage_plugin_${plugin}`, + }, + }; + } else { + knexConfig = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }; + } + + const database = knex(knexConfig); database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); }); From 5f025442dc05eb4d5f285f3bdb2b98b3d57ac75c Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 23 Jul 2020 22:12:53 +0200 Subject: [PATCH 37/54] feat(cli): add e2e test for backend --- .github/workflows/cli.yml | 17 ++ packages/cli/e2e-test/cli-e2e-test.js | 98 ++++++- packages/cli/e2e-test/helpers.js | 6 + packages/cli/package.json | 2 + .../cli/src/commands/create-app/createApp.ts | 6 +- .../cli/templates/default-app/.eslintrc.js | 2 +- .../packages/backend/src/index.ts.hbs | 11 +- yarn.lock | 270 +++++++++++++++++- 8 files changed, 399 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 7704aa36cd..13b84a555f 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -13,6 +13,18 @@ jobs: build: runs-on: ${{ matrix.os }} + services: + postgres: + image: postgres:10.8 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432/tcp + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + strategy: matrix: os: [ubuntu-latest] @@ -44,6 +56,11 @@ jobs: - run: yarn tsc - run: yarn build - name: verify app and plugin creation + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres run: | sudo sysctl fs.inotify.max_user_watches=524288 node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index ef5d6ab243..37c1904b6d 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -16,6 +16,7 @@ const os = require('os'); const fs = require('fs-extra'); +const fetch = require('node-fetch'); const killTree = require('tree-kill'); const { resolve: resolvePath, join: joinPath } = require('path'); const Browser = require('zombie'); @@ -28,6 +29,7 @@ const { waitForExit, print, } = require('./helpers'); +const pgtools = require('pgtools'); async function main() { const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); @@ -36,8 +38,9 @@ async function main() { print('Building dist workspace'); const workspaceDir = await buildDistWorkspace('workspace', rootDir); + const isPostgres = Boolean(process.env.POSTGRES_USER); print('Creating a Backstage App'); - const appDir = await createApp('test-app', workspaceDir, rootDir); + const appDir = await createApp('test-app', isPostgres, workspaceDir, rootDir); print('Creating a Backstage Plugin'); const pluginName = await createPlugin('test-plugin', appDir); @@ -45,6 +48,9 @@ async function main() { print('Starting the app'); await testAppServe(pluginName, appDir); + print('Testing the backend startup'); + await testBackendStart(appDir, isPostgres); + print('All tests successful, removing test dir'); await fs.remove(rootDir); } @@ -97,7 +103,7 @@ async function pinYarnVersion(dir) { /** * Creates a new app inside rootDir called test-app, using packages from the workspaceDir */ -async function createApp(appName, workspaceDir, rootDir) { +async function createApp(appName, isPostgres, workspaceDir, rootDir) { const child = spawnPiped( [ 'node', @@ -119,6 +125,14 @@ async function createApp(appName, workspaceDir, rootDir) { await waitFor(() => stdout.includes('Enter a name for the app')); child.stdin.write(`${appName}\n`); + await waitFor(() => stdout.includes('Select database for the backend')); + + if (!isPostgres) { + // Simulate down arrow press + child.stdin.write(`\u001B\u005B\u0042`); + } + child.stdin.write(`\n`); + print('Waiting for app create script to be done'); await waitForExit(child); @@ -250,5 +264,85 @@ async function testAppServe(pluginName, appDir) { } } +/** Creates PG databases (drops if exists before) */ +async function createDB(database) { + const config = { + host: process.env.POSTGRES_HOST, + port: process.env.POSTGRES_PORT, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + }; + + try { + await pgtools.dropdb({ config }, database); + } catch (_) { + /* do nothing*/ + } + return pgtools.createdb(config, database); +} + +/** + * Start serving the newly created backend and make sure that all db migrations works correctly + */ +async function testBackendStart(appDir, isPostgres) { + if (isPostgres) { + print('Creating DBs'); + await Promise.all( + [ + 'catalog', + 'scaffolder', + 'auth', + 'identity', + 'proxy', + 'techdocs', + ].map(name => createDB(`backstage_plugin_${name}`)), + ); + print('Created DBs'); + } + + const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], { + cwd: appDir, + }); + + let stdout = ''; + let stderr = ''; + child.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + child.stderr.on('data', data => { + stderr = stderr + data.toString('utf8'); + }); + let successful = false; + + try { + await waitFor(() => stdout.includes('Listening on ') || stderr !== ''); + if (stderr !== '') { + // Skipping the whole block + throw new Error(); + } + + print('Try to fetch entities from the backend'); + // Try fetch entities, should be ok + await fetch('http://localhost:7000/catalog/entities').then(res => + res.json(), + ); + print('Entities fetched successfully'); + successful = true; + } finally { + print('Stopping the child process'); + // Kill entire process group, otherwise we'll end up with hanging serve processes + killTree(child.pid); + } + + try { + await waitForExit(child); + } catch (error) { + if (!successful) { + throw error; + } + print('Backend startup test finished successfully'); + } +} + process.on('unhandledRejection', handleError); main(process.argv.slice(2)).catch(handleError); diff --git a/packages/cli/e2e-test/helpers.js b/packages/cli/e2e-test/helpers.js index da2ec343ab..84807119c3 100644 --- a/packages/cli/e2e-test/helpers.js +++ b/packages/cli/e2e-test/helpers.js @@ -76,6 +76,12 @@ function handleError(err) { } } +/** + * Waits for fn() to be true + * Checks every 100ms + * .cancel() is available + * @returns {Promise} Promise of resolution + */ function waitFor(fn) { return new Promise(resolve => { const handle = setInterval(() => { diff --git a/packages/cli/package.json b/packages/cli/package.json index ed7ae9725b..f99f2a7741 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -67,7 +67,9 @@ "jest-css-modules": "^2.1.0", "jest-esm-transformer": "^1.0.0", "mini-css-extract-plugin": "^0.9.0", + "node-fetch": "^2.6.0", "ora": "^4.0.3", + "pgtools": "^0.3.0", "raw-loader": "^4.0.1", "react": "^16.0.0", "react-dev-utils": "^10.2.1", diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index d0fc5e4450..b3ddcd3109 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { promisify } from 'util'; import chalk from 'chalk'; import { Command } from 'commander'; -import inquirer, { Answers, Question, ChoiceBase, ui } from 'inquirer'; +import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import os from 'os'; @@ -113,8 +113,8 @@ export default async (cmd: Command): Promise => { }, ]; const answers: Answers = await inquirer.prompt(questions); - answers['dbTypePG'] = answers.dbType === 'PostgreSQL'; - answers['dbTypeSqlite'] = answers.dbType === 'SQLite'; + answers.dbTypePG = answers.dbType === 'PostgreSQL'; + answers.dbTypeSqlite = answers.dbType === 'SQLite'; const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); diff --git a/packages/cli/templates/default-app/.eslintrc.js b/packages/cli/templates/default-app/.eslintrc.js index 13573efa9c..e351352491 100644 --- a/packages/cli/templates/default-app/.eslintrc.js +++ b/packages/cli/templates/default-app/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], + root: true, }; diff --git a/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs index d1cf3d1ada..5e98afb6d1 100644 --- a/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs +++ b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs @@ -29,7 +29,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -import knex from 'knex'; +import knex, { PgConnectionConfig } from 'knex'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; @@ -49,11 +49,12 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { client: 'pg', useNullAsDefault: true, connection: { - host: process.env.PG_HOST, - user: process.env.PG_USER, - password: process.env.PG_PASSWORD, + port: process.env.POSTGRES_PORT, + host: process.env.POSTGRES_HOST, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, database: `backstage_plugin_${plugin}`, - }, + } as PgConnectionConfig, }; {{/if}} {{#if dbTypeSqlite}} diff --git a/yarn.lock b/yarn.lock index 6c0ff99388..85f2b5b994 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5669,6 +5669,16 @@ buffer-indexof@^1.0.0: resolved "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== +buffer-writer@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz#22a936901e3029afcd7547eb4487ceb697a3bf08" + integrity sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg= + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -5872,6 +5882,11 @@ camelcase@^2.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= + camelcase@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -6169,6 +6184,15 @@ clipboard@^2.0.0: select "^1.1.2" tiny-emitter "^2.0.0" +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -7301,7 +7325,7 @@ decamelize-keys@^1.0.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -9289,6 +9313,11 @@ generic-names@^2.0.1: dependencies: loader-utils "^1.1.0" +generic-pool@2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff" + integrity sha1-eAw29p360FpaBF3Te+etyhGk9v8= + genfun@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" @@ -9299,6 +9328,11 @@ gensync@^1.0.0-beta.1: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -10585,6 +10619,11 @@ invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -11626,6 +11665,11 @@ js-cookie@^2.2.1: resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== +js-string-escape@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" + integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8= + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -12034,6 +12078,13 @@ lazy-universal-dotenv@^3.0.1: dotenv "^8.0.0" dotenv-expand "^5.1.0" +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= + dependencies: + invert-kv "^1.0.0" + left-pad@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" @@ -12303,6 +12354,11 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash.assign@^4.1.0, lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -13659,6 +13715,11 @@ oauth@0.9.x: resolved "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= +object-assign@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" + integrity sha1-ejs9DpgGPUP0wD8uiubNUahog6A= + object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -13903,6 +13964,13 @@ os-homedir@^1.0.0: resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= + dependencies: + lcid "^1.0.0" + os-name@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" @@ -14076,6 +14144,16 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" +packet-reader@0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-0.3.1.tgz#cd62e60af8d7fea8a705ec4ff990871c46871f27" + integrity sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc= + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -14444,11 +14522,111 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +pg-connection-string@0.1.3, pg-connection-string@^0.1.3: + version "0.1.3" + resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" + integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= + pg-connection-string@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.2.0.tgz#caab4d38a9de4fdc29c9317acceed752897de41c" integrity sha512-xB/+wxcpFipUZOQcSzcgkjcNOosGhEoPSjz06jC89lv1dj7mc9bZv6wLVy8M2fVjP0a/xN0N988YDq1L0FhK3A== +pg-connection-string@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz#c13fcb84c298d0bfa9ba12b40dd6c23d946f55d6" + integrity sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@1.*: + version "1.8.0" + resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37" + integrity sha1-9+xzgkw3oD8Hb1G/33DjQBR8Tzc= + dependencies: + generic-pool "2.4.3" + object-assign "4.1.0" + +pg-pool@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.1.tgz#5f4afc0f58063659aeefa952d36af49fa28b30e0" + integrity sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA== + +pg-protocol@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.2.5.tgz#28a1492cde11646ff2d2d06bdee42a3ba05f126c" + integrity sha512-1uYCckkuTfzz/FCefvavRywkowa6M5FohNMF5OjKrqo9PSR8gYc8poVmwwYQaBxhmQdBjhtP514eXy9/Us2xKg== + +pg-types@1.*: + version "1.13.0" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63" + integrity sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ== + dependencies: + pg-int8 "1.0.1" + postgres-array "~1.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.0" + postgres-interval "^1.1.0" + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^6.1.0: + version "6.4.2" + resolved "https://registry.npmjs.org/pg/-/pg-6.4.2.tgz#c364011060eac7a507a2ae063eb857ece910e27f" + integrity sha1-w2QBEGDqx6UHoq4GPrhX7OkQ4n8= + dependencies: + buffer-writer "1.0.1" + js-string-escape "1.0.1" + packet-reader "0.3.1" + pg-connection-string "0.1.3" + pg-pool "1.*" + pg-types "1.*" + pgpass "1.*" + semver "4.3.2" + +pg@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/pg/-/pg-8.3.0.tgz#941383300d38eef51ecb88a0188cec441ab64d81" + integrity sha512-jQPKWHWxbI09s/Z9aUvoTbvGgoj98AU7FDCcQ7kdejupn/TcNpx56v2gaOTzXkzOajmOEJEdi9eTh9cA2RVAjQ== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.3.0" + pg-pool "^3.2.1" + pg-protocol "^1.2.5" + pg-types "^2.1.0" + pgpass "1.x" + semver "4.3.2" + +pgpass@1.*, pgpass@1.x: + version "1.0.2" + resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" + integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= + dependencies: + split "^1.0.0" + +pgtools@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/pgtools/-/pgtools-0.3.0.tgz#ee7decf4183ada28299c63df71e1e73c95b5bc53" + integrity sha512-8NxDCJ8xJ6hOp9hVNZqxi+TZl7hM1Jc8pQyj8DlAbyaWnk5OsGwf3gB/UyDODdOguiim9QzbzPsslp//apO+Uw== + dependencies: + bluebird "^3.3.5" + pg "^6.1.0" + pg-connection-string "^0.1.3" + yargs "^5.0.0" + picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -14980,6 +15158,33 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postgres-array@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5" + integrity sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ== + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.0, postgres-date@~1.0.4: + version "1.0.5" + resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.5.tgz#710b27de5f27d550f6e80b5d34f7ba189213c2ee" + integrity sha512-pdau6GRPERdAYUQwkBnGKxEfPyhVZXG/JiS44iZWiNdSOWE09N2lUgN6yshuq6fVSon4Pm0VMXd1srUUkLe9iA== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -16304,6 +16509,11 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -16747,6 +16957,11 @@ semver-regex@^2.0.0: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" + integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= + semver@7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -17524,7 +17739,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1: +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -19316,6 +19531,11 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^5.0.0" +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= + which-module@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -19354,6 +19574,11 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +window-size@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" + integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= + windows-release@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -19408,6 +19633,14 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" @@ -19599,6 +19832,11 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + y18n@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -19650,6 +19888,14 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f" + integrity sha1-UIE1XRnZ0MjF2BrakIy05tGGZk8= + dependencies: + camelcase "^3.0.0" + lodash.assign "^4.1.0" + yargs@^13.3.2: version "13.3.2" resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -19700,6 +19946,26 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.1" +yargs@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-5.0.0.tgz#3355144977d05757dbb86d6e38ec056123b3a66e" + integrity sha1-M1UUSXfQV1fbuG1uOOwFYSOzpm4= + dependencies: + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + lodash.assign "^4.2.0" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + window-size "^0.2.0" + y18n "^3.2.1" + yargs-parser "^3.2.0" + yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From a6b8602378c0490e8c1bfef4a589717220bb9111 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Fri, 24 Jul 2020 04:01:21 +0200 Subject: [PATCH 38/54] fix: tsc and pr comments --- packages/backend/src/index.ts | 11 ++++++----- .../default-app/packages/backend/src/index.ts.hbs | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 8b9edec327..0da700d205 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -29,7 +29,7 @@ import { } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -import knex from 'knex'; +import knex, { PgConnectionConfig } from 'knex'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; @@ -59,11 +59,12 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { client: 'pg', useNullAsDefault: true, connection: { - host: process.env.PG_HOST, - user: process.env.PG_USER, - password: process.env.PG_PASSWORD, + port: process.env.POSTGRES_PORT, + host: process.env.POSTGRES_HOST, + user: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, database: `backstage_plugin_${plugin}`, - }, + } as PgConnectionConfig, }; } else { knexConfig = { diff --git a/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs index 5e98afb6d1..591fa1a28f 100644 --- a/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs +++ b/packages/cli/templates/default-app/packages/backend/src/index.ts.hbs @@ -29,7 +29,12 @@ import { } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; +{{#if dbTypePG}} import knex, { PgConnectionConfig } from 'knex'; +{{/if}} +{{#if dbTypeSqlite}} +import knex from 'knex'; +{{/if}} import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; From bdc24ae35e186743b1b73120df3157b27e9b7aab Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2020 08:49:55 +0000 Subject: [PATCH 39/54] chore(deps-dev): bump @types/jest from 25.2.3 to 26.0.7 Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 25.2.3 to 26.0.7. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest) Signed-off-by: dependabot-preview[bot] --- packages/app/package.json | 2 +- packages/catalog-model/package.json | 2 +- packages/config-loader/package.json | 2 +- packages/config/package.json | 2 +- packages/core-api/package.json | 2 +- packages/core/package.json | 2 +- packages/dev-utils/package.json | 2 +- packages/test-utils-core/package.json | 2 +- packages/test-utils/package.json | 2 +- plugins/catalog/package.json | 2 +- plugins/circleci/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/github-actions/package.json | 2 +- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/register-component/package.json | 2 +- plugins/rollbar/package.json | 2 +- plugins/scaffolder/package.json | 2 +- plugins/sentry/package.json | 2 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs/package.json | 2 +- plugins/welcome/package.json | 2 +- yarn.lock | 8 ++++---- 24 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 4dc2f9c4c1..61f6002883 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -39,7 +39,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/jquery": "^3.3.34", "@types/node": "^12.0.0", "@types/react-dom": "^16.9.8", diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 14212e84e0..22c090fbe2 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -31,7 +31,7 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.16", "@types/express": "^4.17.6", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" }, diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 25512529b8..e149e9f831 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -36,7 +36,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/yup": "^0.28.2" }, diff --git a/packages/config/package.json b/packages/config/package.json index aac52ab023..afac361a8b 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -33,7 +33,7 @@ "lodash": "^4.17.15" }, "devDependencies": { - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, "files": [ diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 0f0251f23c..69df9cae15 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -46,7 +46,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/zen-observable": "^0.8.0", "jest-fetch-mock": "^3.0.3" diff --git a/packages/core/package.json b/packages/core/package.json index 20a947a202..393f33b7ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,7 +61,7 @@ "@testing-library/user-event": "^12.0.7", "@types/classnames": "^2.2.9", "@types/google-protobuf": "^3.7.2", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react-helmet": "^5.0.15", "@types/zen-observable": "^0.8.0", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 24e54a5cbb..07f74acd92 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -46,7 +46,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, "files": [ diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 10e5b0ea6b..f13021c00b 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -36,7 +36,7 @@ "react-dom": "^16.12.0" }, "devDependencies": { - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, "files": [ diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ef595f53ed..b2bf531355 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, "files": [ diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 4da687b86e..554e66854f 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -45,7 +45,7 @@ "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", "msw": "^0.19.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index b5dfcb3211..3cf357bbce 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react-lazylog": "^4.5.0", "jest-fetch-mock": "^3.0.3" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 4dc8542a12..6840cf5e52 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -38,7 +38,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index ce0068c35a..e6feb1a314 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 80063c6295..8157cfe519 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -37,7 +37,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 2d5f727f4e..193fe3d133 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -50,7 +50,7 @@ "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/codemirror": "^0.0.96", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", "react-router-dom": "6.0.0-beta.0" diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index f9fd64ee69..eea841c174 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -39,7 +39,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 1dcbb86a9c..e1a095f1e4 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 2055cc9525..d2308b869e 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -42,7 +42,7 @@ "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", "jest-fetch-mock": "^3.0.3" diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index a52acfed43..e28387c30e 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -47,7 +47,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 1557ca409a..3a5cbd5094 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -39,7 +39,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 1b801beebd..d72be7af8e 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -42,7 +42,7 @@ "@testing-library/user-event": "^12.0.7", "@types/color": "^3.0.1", "@types/d3-force": "^1.2.1", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", "jest-fetch-mock": "^3.0.3" diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index dfd8393b5f..210d297c7d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 71a19d59bc..66fec93779 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -37,7 +37,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, diff --git a/yarn.lock b/yarn.lock index 6c0ff99388..07a46a31d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3655,10 +3655,10 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^25.2.2": - version "25.2.3" - resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf" - integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw== +"@types/jest@*", "@types/jest@^26.0.7": + version "26.0.7" + resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.7.tgz#495cb1d1818c1699dbc3b8b046baf1c86ef5e324" + integrity sha512-+x0077/LoN6MjqBcVOe1y9dpryWnfDZ+Xfo3EqGeBcfPRJlQp3Lw62RvNlWxuGv7kOEwlHriAa54updi3Jvvwg== dependencies: jest-diff "^25.2.1" pretty-format "^25.2.1" From 88e3581ebcec6111189388d433f843ce6e48c06a Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Sat, 11 Jul 2020 15:38:16 -0400 Subject: [PATCH 40/54] feat: add backend status check handler middleware The statusCheckHandler is a simple express middleware that helps implement healthcheck/readiness endpoints. It can provided an optional status function that should return a promise that resolves in either true or false. The response will be either a 200 or 503 respectively. Any errors that occur will be forwarded to next. --- .../backend-common/src/middleware/index.ts | 1 + .../src/middleware/statusCheckHandler.test.ts | 55 +++++++++++++++++++ .../src/middleware/statusCheckHandler.ts | 51 +++++++++++++++++ .../src/service/lib/ServiceBuilderImpl.ts | 6 +- packages/backend-common/src/service/types.ts | 4 +- packages/backend/src/index.ts | 2 + 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 packages/backend-common/src/middleware/statusCheckHandler.test.ts create mode 100644 packages/backend-common/src/middleware/statusCheckHandler.ts diff --git a/packages/backend-common/src/middleware/index.ts b/packages/backend-common/src/middleware/index.ts index 083b36c3e9..76c52d8830 100644 --- a/packages/backend-common/src/middleware/index.ts +++ b/packages/backend-common/src/middleware/index.ts @@ -17,3 +17,4 @@ export * from './errorHandler'; export * from './notFoundHandler'; export * from './requestLoggingHandler'; +export * from './statusCheckHandler'; diff --git a/packages/backend-common/src/middleware/statusCheckHandler.test.ts b/packages/backend-common/src/middleware/statusCheckHandler.test.ts new file mode 100644 index 0000000000..7ed1a65b58 --- /dev/null +++ b/packages/backend-common/src/middleware/statusCheckHandler.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import request from 'supertest'; +import { statusCheckHandler } from './statusCheckHandler'; + +describe('statusCheckHandler', () => { + it('gives status 200 when using default', async () => { + const app = express(); + app.use('/healthcheck', await statusCheckHandler()); + + const response = await request(app).get('/healthcheck'); + + expect(response.status).toBe(200); + expect(response.text).toBe(JSON.stringify({ status: 'ok' })); + }); + + it('gives status 200 when status function returns true', async () => { + const app = express(); + const status = { foo: 'bar' }; + const statusCheck = () => Promise.resolve(status); + app.use('/healthcheck', await statusCheckHandler({ statusCheck })); + + const response = await request(app).get('/healthcheck'); + + expect(response.status).toBe(200); + expect(response.text).toBe(JSON.stringify(status)); + }); + + it('gives status 500 when status check throws an error', async () => { + const app = express(); + const statusCheck = () => { + throw Error('error!'); + }; + app.use('/healthcheck', await statusCheckHandler({ statusCheck })); + + const response = await request(app).get('/healthcheck'); + + expect(response.status).toBe(500); + }); +}); diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/middleware/statusCheckHandler.ts new file mode 100644 index 0000000000..3f62f04f59 --- /dev/null +++ b/packages/backend-common/src/middleware/statusCheckHandler.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { NextFunction, Request, Response, RequestHandler } from 'express'; + +export type StatusCheck = () => Promise; + +export interface StatusCheckHandlerOptions { + /** + * Optional status function which returns a message. + */ + statusCheck?: StatusCheck; +} + +/** + * Express middleware for status checks. + * + * This is commonly used to implement healthcheck and readiness routes. + * + * @param options An optional configuration object. + * @returns An Express error request handler + */ +export async function statusCheckHandler( + options: StatusCheckHandlerOptions = {}, +): Promise { + const statusCheck: StatusCheck = options.statusCheck + ? options.statusCheck + : () => Promise.resolve({ status: 'ok' }); + + return async (_request: Request, response: Response, next: NextFunction) => { + try { + const status = await statusCheck(); + response.status(200).header('').send(status); + } catch (err) { + next(err); + } + }; +} diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 4a51fb195c..eb07c03af5 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; -import express, { Router } from 'express'; +import express, { Router, RequestHandler } from 'express'; import helmet from 'helmet'; import { Server } from 'http'; import stoppable from 'stoppable'; @@ -40,7 +40,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private host: string | undefined; private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; - private routers: [string, Router][]; + private routers: [string, Router | RequestHandler][]; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -92,7 +92,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } - addRouter(root: string, router: Router): ServiceBuilder { + addRouter(root: string, router: Router | RequestHandler): ServiceBuilder { this.routers.push([root, router]); return this; } diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index b39df27527..070794969f 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import cors from 'cors'; -import { Router } from 'express'; +import { Router, RequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; @@ -64,7 +64,7 @@ export type ServiceBuilder = { * @param root The root URL to bind to (e.g. "/api/function1") * @param router An express router */ - addRouter(root: string, router: Router): ServiceBuilder; + addRouter(root: string, router: Router | RequestHandler): ServiceBuilder; /** * Starts the server using the given settings. diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d060791f98..9764f633b5 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -25,6 +25,7 @@ import { createServiceBuilder, getRootLogger, + statusCheckHandler, useHotMemoize, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; @@ -73,6 +74,7 @@ async function main() { const service = createServiceBuilder(module) .loadConfig(configReader) + .addRouter('/healthcheck', await statusCheckHandler()) .addRouter('/catalog', await catalog(catalogEnv)) .addRouter('/rollbar', await rollbar(rollbarEnv)) .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) From 2a482bd3ef2572bf9e748103aee6fe3de0c519c7 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Sun, 26 Jul 2020 16:11:23 -0400 Subject: [PATCH 41/54] add status check router --- packages/backend-common/package.json | 1 + .../service/createStatusCheckRouter.test.ts | 56 +++++++++++++++++++ .../src/service/createStatusCheckRouter.ts | 38 +++++++++++++ packages/backend-common/src/service/index.ts | 1 + .../src/service/lib/ServiceBuilderImpl.ts | 6 +- packages/backend/src/index.ts | 5 +- packages/backend/src/plugins/healthcheck.ts | 22 ++++++++ 7 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 packages/backend-common/src/service/createStatusCheckRouter.test.ts create mode 100644 packages/backend-common/src/service/createStatusCheckRouter.ts create mode 100644 packages/backend/src/plugins/healthcheck.ts diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 3c944846d1..18131236df 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -35,6 +35,7 @@ "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", + "express-promise-router": "^3.0.3", "helmet": "^3.22.0", "morgan": "^1.10.0", "stoppable": "^1.1.0", diff --git a/packages/backend-common/src/service/createStatusCheckRouter.test.ts b/packages/backend-common/src/service/createStatusCheckRouter.test.ts new file mode 100644 index 0000000000..6c15e7b5f9 --- /dev/null +++ b/packages/backend-common/src/service/createStatusCheckRouter.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import * as winston from 'winston'; +import request from 'supertest'; +import { createStatusCheckRouter } from './createStatusCheckRouter'; + +describe('createStatusCheckRouter', () => { + const logger = winston.createLogger(); + + it('gives status 200 when using default path', async () => { + const app = express(); + app.use('', await createStatusCheckRouter({ logger })); + + const response = await request(app).get('/healthcheck'); + + expect(response.status).toBe(200); + expect(response.text).toBe(JSON.stringify({ status: 'ok' })); + }); + + it('gives status 200 when using custom path', async () => { + const app = express(); + app.use('', await createStatusCheckRouter({ logger, path: '/ready' })); + + const response = await request(app).get('/ready'); + + expect(response.status).toBe(200); + expect(response.text).toBe(JSON.stringify({ status: 'ok' })); + }); + + it('gives status 500 when status check throws an error', async () => { + const app = express(); + const statusCheck = () => { + throw Error('error!'); + }; + app.use('', await createStatusCheckRouter({ logger, statusCheck })); + + const response = await request(app).get('/healthcheck'); + + expect(response.status).toBe(500); + }); +}); diff --git a/packages/backend-common/src/service/createStatusCheckRouter.ts b/packages/backend-common/src/service/createStatusCheckRouter.ts new file mode 100644 index 0000000000..c6014cbfc2 --- /dev/null +++ b/packages/backend-common/src/service/createStatusCheckRouter.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Logger } from 'winston'; +import Router from 'express-promise-router'; +import express from 'express'; +import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware'; + +export interface StatusCheckRouterOptions { + logger: Logger; + path?: string; + statusCheck?: StatusCheck; +} + +export async function createStatusCheckRouter( + options: StatusCheckRouterOptions, +): Promise { + const router = Router(); + const { path = '/healthcheck', statusCheck } = options; + + router.use(path, await statusCheckHandler({ statusCheck })); + router.use(errorHandler()); + + return router; +} diff --git a/packages/backend-common/src/service/index.ts b/packages/backend-common/src/service/index.ts index 9bba9d6baf..58e310032d 100644 --- a/packages/backend-common/src/service/index.ts +++ b/packages/backend-common/src/service/index.ts @@ -15,4 +15,5 @@ */ export { createServiceBuilder } from './createServiceBuilder'; +export { createStatusCheckRouter } from './createStatusCheckRouter'; export type { ServiceBuilder } from './types'; diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index eb07c03af5..4a51fb195c 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; -import express, { Router, RequestHandler } from 'express'; +import express, { Router } from 'express'; import helmet from 'helmet'; import { Server } from 'http'; import stoppable from 'stoppable'; @@ -40,7 +40,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private host: string | undefined; private logger: Logger | undefined; private corsOptions: cors.CorsOptions | undefined; - private routers: [string, Router | RequestHandler][]; + private routers: [string, Router][]; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -92,7 +92,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } - addRouter(root: string, router: Router | RequestHandler): ServiceBuilder { + addRouter(root: string, router: Router): ServiceBuilder { this.routers.push([root, router]); return this; } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9764f633b5..a330353fa8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -25,12 +25,12 @@ import { createServiceBuilder, getRootLogger, - statusCheckHandler, useHotMemoize, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; import knex from 'knex'; +import healthcheck from './plugins/healthcheck'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; import identity from './plugins/identity'; @@ -63,6 +63,7 @@ async function main() { const configReader = ConfigReader.fromConfigs(configs); const createEnv = makeCreateEnv(configs); + const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); const authEnv = useHotMemoize(module, () => createEnv('auth')); @@ -74,7 +75,7 @@ async function main() { const service = createServiceBuilder(module) .loadConfig(configReader) - .addRouter('/healthcheck', await statusCheckHandler()) + .addRouter('', await healthcheck(healthcheckEnv)) .addRouter('/catalog', await catalog(catalogEnv)) .addRouter('/rollbar', await rollbar(rollbarEnv)) .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) diff --git a/packages/backend/src/plugins/healthcheck.ts b/packages/backend/src/plugins/healthcheck.ts new file mode 100644 index 0000000000..3dd9a67827 --- /dev/null +++ b/packages/backend/src/plugins/healthcheck.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createStatusCheckRouter } from '@backstage/backend-common'; +import { PluginEnvironment } from '../types'; + +export default async function createRouter({ logger }: PluginEnvironment) { + return await createStatusCheckRouter({ logger, path: '/healthcheck' }); +} From 60d3ecc695e69977185fb48efe098a087eb99521 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Fri, 17 Jul 2020 13:24:30 +0200 Subject: [PATCH 42/54] Read auth providers in backend and frontend from app-config yaml --- app-config.yaml | 69 ++++++++++++++++ packages/app/src/App.tsx | 15 ++-- packages/backend/src/index.ts | 2 +- .../core/src/layout/Sidebar/UserSettings.tsx | 64 +++++++++------ .../core/src/layout/SignInPage/providers.tsx | 10 +-- .../auth-backend/src/providers/factories.ts | 36 ++++++--- .../src/providers/github/provider.ts | 66 +++++++-------- .../src/providers/gitlab/provider.ts | 73 ++++++++--------- .../src/providers/google/provider.ts | 64 +++++++-------- .../src/providers/oauth2/provider.ts | 81 +++++++++---------- .../src/providers/okta/provider.ts | 68 +++++++--------- .../src/providers/saml/provider.ts | 44 ++++------ plugins/auth-backend/src/providers/types.ts | 8 +- plugins/auth-backend/src/service/router.ts | 68 +--------------- 14 files changed, 333 insertions(+), 335 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 9a4a14b923..e76aba266e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -25,3 +25,72 @@ techdocs: sentry: organization: spotify + +auth: + providers: + google: + development: + appOrigin: "http://localhost:3000/" + secure: false + clientId: + $secret: + env: AUTH_GOOGLE_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GOOGLE_CLIENT_SECRET + github: + development: + appOrigin: "http://localhost:3000/" + secure: false + clientId: + $secret: + env: AUTH_GITHUB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITHUB_CLIENT_SECRET + gitlab: + development: + appOrigin: "http://localhost:3000/" + secure: false + clientId: + $secret: + env: AUTH_GITLAB_CLIENT_ID + clientSecret: + $secret: + env: AUTH_GITLAB_CLIENT_SECRET + audience: + $secret: + env: GITLAB_BASE_URL + # saml: + # development: + # entryPoint: "http://localhost:7001/" + # issuer: "passport-saml" + okta: + development: + appOrigin: "http://localhost:3000/" + secure: false + clientId: + $secret: + env: AUTH_OKTA_CLIENT_ID + clientSecret: + $secret: + env: AUTH_OKTA_CLIENT_SECRET + audience: + $secret: + env: AUTH_OKTA_AUDIENCE + oauth2: + development: + appOrigin: "http://localhost:3000/" + secure: false + clientId: + $secret: + env: AUTH_OAUTH2_CLIENT_ID + clientSecret: + $secret: + env: AUTH_OAUTH2_CLIENT_SECRET + authorizationURL: + $secret: + env: AUTH_OAUTH2_AUTH_URL + tokenURL: + $secret: + env: AUTH_OAUTH2_TOKEN_URL diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index c569308fc7..497b016c5c 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -19,6 +19,8 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + useApi, + configApiRef, } from '@backstage/core'; import React, { FC } from 'react'; import Root from './components/Root'; @@ -30,12 +32,13 @@ const app = createApp({ apis, plugins: Object.values(plugins), components: { - SignInPage: props => ( - - ), + SignInPage: props => { + const configApi = useApi(configApiRef); + const providersConfig = configApi.getConfig('auth.providers'); + const providers = providersConfig.keys(); + + return ; + }, }, }); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d060791f98..a8b862c423 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -58,7 +58,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { } async function main() { - const configs = await loadConfig(); + const configs = await loadConfig({ shouldReadSecrets: true }); const configReader = ConfigReader.fromConfigs(configs); const createEnv = makeCreateEnv(configs); diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 5f66fc6558..98362a6b9a 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -22,6 +22,7 @@ import { oauth2ApiRef, oktaAuthApiRef, useApi, + configApiRef, } from '@backstage/core-api'; import Collapse from '@material-ui/core/Collapse'; import SignOutIcon from '@material-ui/icons/MeetingRoom'; @@ -39,6 +40,9 @@ export function SidebarUserSettings() { const { isOpen: sidebarOpen } = useContext(SidebarContext); const [open, setOpen] = React.useState(false); const identityApi = useApi(identityApiRef); + const configApi = useApi(configApiRef); + const providersConfig = configApi.getConfig('auth.providers'); + const providers = providersConfig.keys(); // Close the provider list when sidebar collapse useEffect(() => { @@ -49,31 +53,41 @@ export function SidebarUserSettings() { <> - - - - - + {providers.includes('google') && ( + + )} + {providers.includes('github') && ( + + )} + {providers.includes('gitlab') && ( + + )} + {providers.includes('okta') && ( + + )} + {providers.includes('oauth2') && ( + + )} { @@ -46,17 +51,28 @@ export const createAuthProviderRouter = ( throw Error(`No auth provider available for '${providerId}'`); } - const provider = factory(globalConfig, providerConfig, logger, issuer); - const router = Router(); - router.get('/start', provider.start.bind(provider)); - router.get('/handler/frame', provider.frameHandler.bind(provider)); - router.post('/handler/frame', provider.frameHandler.bind(provider)); - if (provider.logout) { - router.post('/logout', provider.logout.bind(provider)); + const envs = providerConfig.keys(); + const envProviders: EnvironmentHandlers = {}; + + for (const env of envs) { + const envConfig = providerConfig.getConfig(env); + const provider = factory(globalConfig, env, envConfig, logger, issuer); + if (provider) { + envProviders[env] = provider; + } } - if (provider.refresh) { - router.get('/refresh', provider.refresh.bind(provider)); + + const handler = new EnvironmentHandler(providerId, envProviders); + + router.get('/start', handler.start.bind(handler)); + router.get('/handler/frame', handler.frameHandler.bind(handler)); + router.post('/handler/frame', handler.frameHandler.bind(handler)); + if (handler.logout) { + router.post('/logout', handler.logout.bind(handler)); + } + if (handler.refresh) { + router.get('/refresh', handler.refresh.bind(handler)); } return router; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index bd7d4aa712..a67e78febd 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -25,20 +25,15 @@ import { OAuthProviderHandlers, AuthProviderConfig, RedirectInfo, - EnvironmentProviderConfig, OAuthProviderOptions, - OAuthProviderConfig, OAuthResponse, PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; -import { - EnvironmentHandlers, - EnvironmentHandler, -} from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; +import { Config } from '@backstage/config'; export class GithubAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GithubStrategy; @@ -131,44 +126,43 @@ export class GithubAuthProvider implements OAuthProviderHandlers { export function createGithubProvider( { baseUrl }: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { const providerId = 'github'; - const envProviders: EnvironmentHandlers = {}; + const secure = envConfig.getBoolean('secure'); + const appOrigin = envConfig.getString('appOrigin'); + const clientID = envConfig.getOptionalString('clientId'); + const clientSecret = envConfig.getOptionalString('clientSecret'); + const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; - for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as OAuthProviderConfig; - const { secure, appOrigin } = config; - const opts = { - clientID: config.clientId, - clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, - }; + const opts = { + clientID, + clientSecret, + callbackURL, + }; - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable', + if (!opts.clientID || !opts.clientSecret) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars', ); - continue; } - envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), { - disableRefresh: true, - persistScopes: true, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, - }); + logger.warn( + 'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable', + ); + return undefined; } - return new EnvironmentHandler(providerId, envProviders); + return new OAuthProvider(new GithubAuthProvider(opts), { + disableRefresh: true, + persistScopes: true, + providerId, + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index af1d57fcce..78b1a55211 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -25,20 +25,15 @@ import { OAuthProviderHandlers, AuthProviderConfig, RedirectInfo, - EnvironmentProviderConfig, OAuthProviderOptions, - OAuthProviderConfig, OAuthResponse, PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; -import { - EnvironmentHandlers, - EnvironmentHandler, -} from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; import passport from 'passport'; +import { Config } from '@backstage/config'; export class GitlabAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GitlabStrategy; @@ -138,49 +133,45 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { export function createGitlabProvider( { baseUrl }: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { const providerId = 'gitlab'; - const envProviders: EnvironmentHandlers = {}; + const secure = envConfig.getBoolean('secure'); + const appOrigin = envConfig.getString('appOrigin'); + const clientID = envConfig.getOptionalString('clientId'); + const clientSecret = envConfig.getOptionalString('clientSecret'); + const audience = envConfig.getOptionalString('audience'); + const baseURL = audience || 'https://gitlab.com'; + const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; - for (const [env, envConfig] of Object.entries(providerConfig)) { - const { - secure, - appOrigin, - clientId, - clientSecret, - audience, - } = (envConfig as unknown) as OAuthProviderConfig; - const opts = { - clientID: clientId, - clientSecret: clientSecret, - callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, - baseURL: audience, - }; + const opts = { + clientID, + clientSecret, + callbackURL, + baseURL, + }; - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable', + if (!opts.clientID || !opts.clientSecret) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars', ); - continue; } - envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), { - disableRefresh: true, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, - }); + logger.warn( + 'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable', + ); + return undefined; } - return new EnvironmentHandler(providerId, envProviders); + return new OAuthProvider(new GitlabAuthProvider(opts), { + disableRefresh: true, + providerId, + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 3023c09886..5749d1465f 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -27,20 +27,15 @@ import { OAuthProviderHandlers, RedirectInfo, AuthProviderConfig, - EnvironmentProviderConfig, OAuthProviderOptions, - OAuthProviderConfig, OAuthResponse, PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; import passport from 'passport'; -import { - EnvironmentHandler, - EnvironmentHandlers, -} from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; +import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -150,43 +145,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { export function createGoogleProvider( { baseUrl }: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { const providerId = 'google'; - const envProviders: EnvironmentHandlers = {}; + const secure = envConfig.getBoolean('secure'); + const appOrigin = envConfig.getString('appOrigin'); + const clientID = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; - for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as OAuthProviderConfig; - const { secure, appOrigin } = config; - const opts = { - clientID: config.clientId, - clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, - }; + const opts = { + clientID, + clientSecret, + callbackURL, + }; - if (!opts.clientID || !opts.clientSecret) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars', - ); - } - - logger.warn( - 'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable', + if (!opts.clientID || !opts.clientSecret) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars', ); - continue; } - envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, - }); + logger.warn( + 'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable', + ); + return undefined; } - return new EnvironmentHandler(providerId, envProviders); + return new OAuthProvider(new GoogleAuthProvider(opts), { + disableRefresh: false, + providerId, + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); } diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 60b6bc605b..e4258605ac 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -19,10 +19,6 @@ import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; -import { - EnvironmentHandler, - EnvironmentHandlers, -} from '../../lib/EnvironmentHandler'; import { OAuthProvider } from '../../lib/OAuthProvider'; import { executeFetchUserProfileStrategy, @@ -33,14 +29,13 @@ import { } from '../../lib/PassportStrategyHelper'; import { AuthProviderConfig, - EnvironmentProviderConfig, - GenericOAuth2ProviderConfig, GenericOAuth2ProviderOptions, OAuthProviderHandlers, OAuthResponse, PassportDoneCallback, RedirectInfo, } from '../types'; +import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -148,51 +143,51 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers { export function createOAuth2Provider( { baseUrl }: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { const providerId = 'oauth2'; - const envProviders: EnvironmentHandlers = {}; + const secure = envConfig.getBoolean('secure'); + const appOrigin = envConfig.getString('appOrigin'); + const clientID = envConfig.getOptionalString('clientId'); + const clientSecret = envConfig.getOptionalString('clientSecret'); + const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; + const authorizationURL = envConfig.getOptionalString('authorizationURL'); + const tokenURL = envConfig.getOptionalString('tokenURL'); - for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as GenericOAuth2ProviderConfig; - const { secure, appOrigin } = config; - const opts = { - clientID: config.clientId, - clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, - authorizationURL: config.authorizationURL, - tokenURL: config.tokenURL, - }; + const opts = { + clientID, + clientSecret, + callbackURL, + authorizationURL, + tokenURL, + }; - if ( - !opts.clientID || - !opts.clientSecret || - !opts.authorizationURL || - !opts.tokenURL - ) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars', - ); - } - - logger.warn( - 'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable', + if ( + !opts.clientID || + !opts.clientSecret || + !opts.authorizationURL || + !opts.tokenURL + ) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars', ); - continue; } - envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, - }); + logger.warn( + 'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable', + ); + return undefined; } - - return new EnvironmentHandler(providerId, envProviders); + return new OAuthProvider(new OAuth2AuthProvider(opts), { + disableRefresh: false, + providerId, + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); } diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 01d7684e97..5fcc7cb778 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -28,19 +28,14 @@ import { OAuthProviderHandlers, RedirectInfo, AuthProviderConfig, - EnvironmentProviderConfig, OAuthProviderOptions, - OAuthProviderConfig, OAuthResponse, PassportDoneCallback, } from '../types'; -import { - EnvironmentHandler, - EnvironmentHandlers, -} from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; import { StateStore } from 'passport-oauth2'; import { TokenIssuer } from '../../identity'; +import { Config } from '@backstage/config'; type PrivateInfo = { refreshToken: string; @@ -170,45 +165,44 @@ export class OktaAuthProvider implements OAuthProviderHandlers { export function createOktaProvider( { baseUrl }: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { const providerId = 'okta'; - const envProviders: EnvironmentHandlers = {}; + const secure = envConfig.getBoolean('secure'); + const appOrigin = envConfig.getString('appOrigin'); + const clientID = envConfig.getOptionalString('clientId'); + const clientSecret = envConfig.getOptionalString('clientSecret'); + const audience = envConfig.getOptionalString('audience'); + const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; - for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as OAuthProviderConfig; - const { secure, appOrigin } = config; - const opts = { - audience: config.audience, - clientID: config.clientId, - clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, - }; + const opts = { + audience, + clientID, + clientSecret, + callbackURL, + }; - if (!opts.clientID || !opts.clientSecret || !opts.audience) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars', - ); - } - - logger.warn( - 'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable', + if (!opts.clientID || !opts.clientSecret || !opts.audience) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars', ); - continue; } - envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), { - disableRefresh: false, - providerId, - secure, - baseUrl, - appOrigin, - tokenIssuer, - }); + logger.warn( + 'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable', + ); + return undefined; } - - return new EnvironmentHandler(providerId, envProviders); + return new OAuthProvider(new OktaAuthProvider(opts), { + disableRefresh: false, + providerId, + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 40c80de4e4..3ddfba4bb0 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -27,18 +27,13 @@ import { import { AuthProviderConfig, AuthProviderRouteHandlers, - EnvironmentProviderConfig, - SAMLProviderConfig, PassportDoneCallback, ProfileInfo, } from '../types'; import { postMessageResponse } from '../../lib/OAuthProvider'; -import { - EnvironmentHandlers, - EnvironmentHandler, -} from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; +import { Config } from '@backstage/config'; type SamlInfo = { userId: string; @@ -122,30 +117,25 @@ type SAMLProviderOptions = { export function createSamlProvider( _authProviderConfig: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + _env: string, + envConfig: Config, logger: Logger, tokenIssuer: TokenIssuer, ) { - const envProviders: EnvironmentHandlers = {}; + const entryPoint = envConfig.getString('entryPoint'); + const issuer = envConfig.getString('issuer'); + const opts = { + entryPoint, + issuer, + path: '/auth/saml/handler/frame', + tokenIssuer, + }; - for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as SAMLProviderConfig; - const opts = { - entryPoint: config.entryPoint, - issuer: config.issuer, - path: '/auth/saml/handler/frame', - tokenIssuer, - }; - - if (!opts.entryPoint || !opts.issuer) { - logger.warn( - 'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable', - ); - continue; - } - - envProviders[env] = new SamlAuthProvider(opts); + if (!opts.entryPoint || !opts.issuer) { + logger.warn( + 'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable', + ); + return undefined; } - - return new EnvironmentHandler('saml', envProviders); + return new SamlAuthProvider(opts); } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index de4e076919..93d459bb0d 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -17,6 +17,9 @@ import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity'; +import { Config } from '@backstage/config'; +import { OAuthProvider } from '../lib/OAuthProvider'; +import { SamlAuthProvider } from './saml/provider'; export type OAuthProviderOptions = { /** @@ -204,10 +207,11 @@ export interface AuthProviderRouteHandlers { export type AuthProviderFactory = ( globalConfig: AuthProviderConfig, - providerConfig: EnvironmentProviderConfig, + env: string, + envConfig: Config, logger: Logger, issuer: TokenIssuer, -) => AuthProviderRouteHandlers; +) => OAuthProvider | SamlAuthProvider | undefined; export type AuthResponse = { providerInfo: ProviderInfo; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 6b296cba2d..69f058f6db 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -36,8 +36,6 @@ export async function createRouter( const router = Router(); const logger = options.logger.child({ plugin: 'auth' }); - const appUrl = new URL(options.config.getString('app.baseUrl')); - const appOrigin = appUrl.origin; const backendUrl = options.config.getString('backend.baseUrl'); const authUrl = `${backendUrl}/auth`; @@ -57,71 +55,13 @@ export async function createRouter( router.use(bodyParser.urlencoded({ extended: false })); router.use(bodyParser.json()); - const config = { - backend: { - baseUrl: backendUrl, - }, - auth: { - providers: { - google: { - development: { - appOrigin, - secure: false, - clientId: process.env.AUTH_GOOGLE_CLIENT_ID!, - clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!, - }, - }, - github: { - development: { - appOrigin, - secure: false, - clientId: process.env.AUTH_GITHUB_CLIENT_ID!, - clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, - }, - }, - gitlab: { - development: { - appOrigin, - secure: false, - clientId: process.env.AUTH_GITLAB_CLIENT_ID!, - clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!, - audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com', - }, - }, - saml: { - development: { - entryPoint: 'http://localhost:7001/', - issuer: 'passport-saml', - }, - }, - okta: { - development: { - appOrigin, - secure: false, - clientId: process.env.AUTH_OKTA_CLIENT_ID!, - clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!, - audience: process.env.AUTH_OKTA_AUDIENCE, - }, - }, - oauth2: { - development: { - appOrigin, - secure: false, - clientId: process.env.AUTH_OAUTH2_CLIENT_ID!, - clientSecret: process.env.AUTH_OAUTH2_CLIENT_SECRET!, - authorizationURL: process.env.AUTH_OAUTH2_AUTH_URL!, - tokenURL: process.env.AUTH_OAUTH2_TOKEN_URL!, - }, - }, - }, - }, - }; + const providersConfig = options.config.getConfig('auth.providers'); + const providers = providersConfig.keys(); - const providerConfigs = config.auth.providers; - - for (const [providerId, providerConfig] of Object.entries(providerConfigs)) { + for (const providerId of providers) { logger.info(`Configuring provider, ${providerId}`); try { + const providerConfig = providersConfig.getConfig(providerId); const providerRouter = createAuthProviderRouter( providerId, { baseUrl: authUrl }, From 19071023cd96b7b79f7f546fe7f96e056d12f9a9 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Fri, 17 Jul 2020 14:13:47 +0200 Subject: [PATCH 43/54] fix type issues --- plugins/auth-backend/src/providers/github/provider.ts | 4 ++-- plugins/auth-backend/src/providers/gitlab/provider.ts | 6 +++--- plugins/auth-backend/src/providers/oauth2/provider.ts | 8 ++++---- plugins/auth-backend/src/providers/okta/provider.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index a67e78febd..37d661f9ab 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -134,8 +134,8 @@ export function createGithubProvider( const providerId = 'github'; const secure = envConfig.getBoolean('secure'); const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getOptionalString('clientId'); - const clientSecret = envConfig.getOptionalString('clientSecret'); + const clientID = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; const opts = { diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 78b1a55211..c5c546c6f3 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -141,9 +141,9 @@ export function createGitlabProvider( const providerId = 'gitlab'; const secure = envConfig.getBoolean('secure'); const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getOptionalString('clientId'); - const clientSecret = envConfig.getOptionalString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); + const clientID = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); const baseURL = audience || 'https://gitlab.com'; const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index e4258605ac..967fd011f0 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -151,11 +151,11 @@ export function createOAuth2Provider( const providerId = 'oauth2'; const secure = envConfig.getBoolean('secure'); const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getOptionalString('clientId'); - const clientSecret = envConfig.getOptionalString('clientSecret'); + const clientID = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; - const authorizationURL = envConfig.getOptionalString('authorizationURL'); - const tokenURL = envConfig.getOptionalString('tokenURL'); + const authorizationURL = envConfig.getString('authorizationURL'); + const tokenURL = envConfig.getString('tokenURL'); const opts = { clientID, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 5fcc7cb778..56e8b1220d 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -173,9 +173,9 @@ export function createOktaProvider( const providerId = 'okta'; const secure = envConfig.getBoolean('secure'); const appOrigin = envConfig.getString('appOrigin'); - const clientID = envConfig.getOptionalString('clientId'); - const clientSecret = envConfig.getOptionalString('clientSecret'); - const audience = envConfig.getOptionalString('audience'); + const clientID = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`; const opts = { From 5ead870b44aeebf100c9ba6c185664fe49e5a298 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Fri, 17 Jul 2020 14:26:21 +0200 Subject: [PATCH 44/54] Add config api to storybook --- packages/app/src/App.tsx | 4 ++-- packages/core/src/layout/Sidebar/UserSettings.tsx | 4 ++-- packages/storybook/.storybook/apis.js | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 497b016c5c..9e9ca1edd0 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -34,8 +34,8 @@ const app = createApp({ components: { SignInPage: props => { const configApi = useApi(configApiRef); - const providersConfig = configApi.getConfig('auth.providers'); - const providers = providersConfig.keys(); + const providersConfig = configApi.getOptionalConfig('auth.providers'); + const providers = providersConfig?.keys() ?? []; return ; }, diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 98362a6b9a..e69d63186d 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -41,8 +41,8 @@ export function SidebarUserSettings() { const [open, setOpen] = React.useState(false); const identityApi = useApi(identityApiRef); const configApi = useApi(configApiRef); - const providersConfig = configApi.getConfig('auth.providers'); - const providers = providersConfig.keys(); + const providersConfig = configApi.getOptionalConfig('auth.providers'); + const providers = providersConfig?.keys() ?? []; // Close the provider list when sidebar collapse useEffect(() => { diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index f4b7557b89..051d95de05 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -18,10 +18,14 @@ import { OAuthRequestManager, OktaAuth, oktaAuthApiRef, + configApiRef, + ConfigReader, } from '@backstage/core'; const builder = ApiRegistry.builder(); +builder.add(configApiRef, ConfigReader.fromConfigs([])); + const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); From 4517f2382dc4c80cc76377a992510fe03a9bcbe3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 10:42:24 +0200 Subject: [PATCH 45/54] packages/cli: avoid packing deps of deps that are marked as bundled --- packages/cli/src/lib/packager/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index d3ba1f6b12..f4a2181b40 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -134,11 +134,15 @@ async function findTargetPackages(pkgNames: string[]): Promise { throw new Error(`Package '${name}' not found`); } - const pkgDeps = Object.keys(node.pkg.dependencies); - const localDeps: string[] = Array.from(node.localDependencies.keys()); - const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep)); + // Don't include dependencies of packages that are marked as bundled + if (!node.pkg.get('bundled')) { + const pkgDeps = Object.keys(node.pkg.dependencies); + const localDeps: string[] = Array.from(node.localDependencies.keys()); + const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep)); + + searchNames.push(...filteredDeps); + } - searchNames.push(...filteredDeps); targets.set(name, node.pkg); } From 3aaf4f0200e5906b3495b8ee48c5d318d8df5687 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 27 Jul 2020 12:52:11 +0200 Subject: [PATCH 46/54] chore(adr): added adr reference to techocs --- docs/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/README.md b/docs/README.md index ee83b24050..c870a144d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,6 +87,7 @@ better yet, a pull request. - [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md) - [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md) - [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md) + - [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md) - [Contribute](../CONTRIBUTING.md) - [Support](overview/support.md) - [FAQ](FAQ.md) From 2aacd157ca60f570f789ddad63dcf7352676f350 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 12:58:02 +0200 Subject: [PATCH 47/54] docgen: bump to alpha.16 --- packages/docgen/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docgen/package.json b/packages/docgen/package.json index f2675eef24..97d057a4b7 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,7 +1,7 @@ { "name": "docgen", "description": "Tool for generating API Documentation for itself", - "version": "0.1.1-alpha.13", + "version": "0.1.1-alpha.16", "private": true, "homepage": "https://backstage.io", "repository": { From bd62f980700ff3f0072e4f2a5661cf2d59c4df45 Mon Sep 17 00:00:00 2001 From: Andrew Thauer <6507159+andrewthauer@users.noreply.github.com> Date: Mon, 27 Jul 2020 07:46:33 -0400 Subject: [PATCH 48/54] fix: logger comment for errorhandler options --- packages/backend-common/src/middleware/errorHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts index 798d173d38..7365ce8b93 100644 --- a/packages/backend-common/src/middleware/errorHandler.ts +++ b/packages/backend-common/src/middleware/errorHandler.ts @@ -28,9 +28,9 @@ export type ErrorHandlerOptions = { showStackTraces?: boolean; /** - * Logger + * Logger instance to log any 5xx errors. * - * If not specified, by default shows stack traces only in development mode. + * If not specified, the root logger will be used. */ logger?: Logger; }; From bf919ca4972cdabe6305747d4f2232d8c7eeb654 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 13:55:50 +0200 Subject: [PATCH 49/54] cli: bump @types/jest in templates --- .../cli/templates/default-app/packages/app/package.json.hbs | 2 +- packages/cli/templates/default-plugin/package.json.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs index 26cce2c5e6..f8fd0135ab 100644 --- a/packages/cli/templates/default-app/packages/app/package.json.hbs +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -22,7 +22,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react-dom": "^16.9.8", "cross-env": "^7.0.0", diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index da611a13b7..d5d25b974a 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -36,7 +36,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", - "@types/jest": "^25.2.2", + "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3" }, From e5ed8f95d1e318044eff61f967fe1fd3b8a8a4b9 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Mon, 27 Jul 2020 14:38:07 +0200 Subject: [PATCH 50/54] fix(cli): e2e backend PR adjustments --- .github/workflows/cli.yml | 2 +- packages/backend/src/index.ts | 10 +++++----- packages/cli/e2e-test/cli-e2e-test.js | 6 ++++-- .../templates/default-app/packages/backend/README.md | 2 -- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 13b84a555f..32fb15d329 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -15,7 +15,7 @@ jobs: services: postgres: - image: postgres:10.8 + image: postgres:latest env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0da700d205..9d2d89a049 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -50,7 +50,7 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { 'POSTGRES_USER', 'POSTGRES_HOST', 'POSTGRES_PASSWORD', - ].every(key => Object.keys(process.env).includes(key)); + ].every(key => config.getOptional(`backend.${key}`)); let knexConfig; @@ -59,10 +59,10 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) { client: 'pg', useNullAsDefault: true, connection: { - port: process.env.POSTGRES_PORT, - host: process.env.POSTGRES_HOST, - user: process.env.POSTGRES_USER, - password: process.env.POSTGRES_PASSWORD, + port: config.getOptionalNumber('backend.POSTGRES_PORT'), + host: config.getString('backend.POSTGRES_HOST'), + user: config.getString('backend.POSTGRES_USER'), + password: config.getString('backend.POSTGRES_PASSWORD'), database: `backstage_plugin_${plugin}`, } as PgConnectionConfig, }; diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index 37c1904b6d..6bab97f66b 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -318,7 +318,7 @@ async function testBackendStart(appDir, isPostgres) { await waitFor(() => stdout.includes('Listening on ') || stderr !== ''); if (stderr !== '') { // Skipping the whole block - throw new Error(); + throw new Error(stderr); } print('Try to fetch entities from the backend'); @@ -328,6 +328,8 @@ async function testBackendStart(appDir, isPostgres) { ); print('Entities fetched successfully'); successful = true; + } catch (error) { + throw new Error(`Backend failed to startup: ${error}`); } finally { print('Stopping the child process'); // Kill entire process group, otherwise we'll end up with hanging serve processes @@ -338,7 +340,7 @@ async function testBackendStart(appDir, isPostgres) { await waitForExit(child); } catch (error) { if (!successful) { - throw error; + throw new Error(`Backend failed to startup: ${stderr}`); } print('Backend startup test finished successfully'); } diff --git a/packages/cli/templates/default-app/packages/backend/README.md b/packages/cli/templates/default-app/packages/backend/README.md index 90c70912f0..f94904a930 100644 --- a/packages/cli/templates/default-app/packages/backend/README.md +++ b/packages/cli/templates/default-app/packages/backend/README.md @@ -29,8 +29,6 @@ AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ -ROLLBAR_ACCOUNT_TOKEN=x \ -SENTRY_TOKEN=x \ LOG_LEVEL=debug \ yarn start ``` From 1baccd1304a46f87880aa5f0a794831cc6ed49ed Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 16:06:11 +0200 Subject: [PATCH 51/54] deps: bump ts, express + friends and fix type issue --- .../src/lib/EnvironmentHandler.ts | 2 +- yarn.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index 76c1f40b73..3eab4e793c 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -33,7 +33,7 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers { ): AuthProviderRouteHandlers | undefined { const env = req.query.env?.toString(); - if (this.providers.hasOwnProperty(env)) { + if (env && this.providers.hasOwnProperty(env)) { return this.providers[env]; } diff --git a/yarn.lock b/yarn.lock index a31c2a75ae..05014c39b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3748,17 +3748,18 @@ integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.5": - version "4.17.5" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c" - integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw== + version "4.17.9" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" + integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== dependencies: "@types/node" "*" + "@types/qs" "*" "@types/range-parser" "*" "@types/express@*", "@types/express@^4.17.6": - version "4.17.6" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e" - integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w== + version "4.17.7" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz#42045be6475636d9801369cd4418ef65cdb0dd59" + integrity sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "*" @@ -4147,9 +4148,9 @@ integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== "@types/qs@*": - version "6.9.1" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" - integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== + version "6.9.4" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== "@types/range-parser@*": version "1.2.3" From a79b844aee956395debcd11195b3d024c9660c09 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 16:16:13 +0200 Subject: [PATCH 52/54] deps: bump @types/node + fix type issues --- packages/cli/src/lib/run.ts | 2 +- yarn.lock | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/lib/run.ts b/packages/cli/src/lib/run.ts index 15b827f0c6..69d3d92640 100644 --- a/packages/cli/src/lib/run.ts +++ b/packages/cli/src/lib/run.ts @@ -92,7 +92,7 @@ export async function runCheck(cmd: string, ...args: string[]) { } export async function waitForExit( - child: ChildProcess & { exitCode?: number }, + child: ChildProcess & { exitCode: number | null }, name?: string, ): Promise { if (typeof child.exitCode === 'number') { diff --git a/yarn.lock b/yarn.lock index 05014c39b0..1464c8e464 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4039,20 +4039,25 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@^13.7.2": - version "13.9.2" - resolved "https://registry.npmjs.org/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349" - integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg== +"@types/node@*", "@types/node@>= 8": + version "14.0.26" + resolved "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c" + integrity sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA== "@types/node@^10.1.0": - version "10.17.27" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.27.tgz#391cb391c75646c8ad2a7b6ed3bbcee52d1bdf19" - integrity sha512-J0oqm9ZfAXaPdwNXMMgAhylw5fhmXkToJd06vuDUSAgEDZ/n/69/69UmyBZbc+zT34UnShuDSBqvim3SPnozJg== + version "10.17.28" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9" + integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ== "@types/node@^12.0.0": - version "12.12.30" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.30.tgz#3501e6f09b954de9c404671cefdbcc5d9d7c45f6" - integrity sha512-sz9MF/zk6qVr3pAnM0BSQvYIBK44tS75QC5N+VbWSE4DjCV/pJ+UzCW/F+vVnl7TkOPcuwQureKNtSSwjBTaMg== + version "12.12.53" + resolved "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129" + integrity sha512-51MYTDTyCziHb70wtGNFRwB4l+5JNvdqzFSkbDvpbftEgVUBEE+T5f7pROhWMp/fxp07oNIEQZd5bbfAH22ohQ== + +"@types/node@^13.7.2": + version "13.13.15" + resolved "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" + integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== "@types/nodegit@0.26.5": version "0.26.5" @@ -4428,12 +4433,7 @@ "@types/serve-static" "*" "@types/webpack" "*" -"@types/webpack-env@^1.15.0": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.1.tgz#c8e84705e08eed430b5e15b39c65b0944e4d1422" - integrity sha512-eWN5ElDTeBc5lRDh95SqA8x18D0ll2pWudU3uWiyfsRmIZcmUXpEsxPU+7+BsdCrO2vfLRC629u/MmjbmF+2tA== - -"@types/webpack-env@^1.15.2": +"@types/webpack-env@^1.15.0", "@types/webpack-env@^1.15.2": version "1.15.2" resolved "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== From acbe95224cdf308320b583ddcff5043b04ffb8ad Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 16:16:50 +0200 Subject: [PATCH 53/54] cli: run tsc again after creating plugin --- packages/cli/e2e-test/cli-e2e-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index ef5d6ab243..625122715d 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -201,7 +201,7 @@ async function createPlugin(pluginName, appDir) { await waitForExit(child); const pluginDir = resolvePath(appDir, 'plugins', pluginName); - for (const cmd of [['lint'], ['test', '--no-watch']]) { + for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) { print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); await runPlain(['yarn', ...cmd], { cwd: pluginDir }); } From a95e33ffae2b5261dc2a4ad237ec362a121616fd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 27 Jul 2020 16:17:00 +0200 Subject: [PATCH 54/54] yarn.lock: sync --- yarn.lock | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yarn.lock b/yarn.lock index 1464c8e464..50c4382286 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20080,6 +20080,11 @@ whatwg-fetch@^2.0.0, whatwg-fetch@^2.0.4: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== +whatwg-fetch@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc" + integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w== + whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"