Merge pull request #1675 from spotify/rugvip/docgen

add docgen package along with generated API References
This commit is contained in:
Patrik Oldsberg
2020-07-27 13:51:34 +02:00
committed by GitHub
38 changed files with 3775 additions and 4 deletions
+2 -1
View File
@@ -77,7 +77,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)
+2 -1
View File
@@ -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`.
+2 -1
View File
@@ -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
+114
View File
@@ -0,0 +1,114 @@
# AlertApi
The AlertApi type is defined at
[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#alert)
## Members
### post()
Post an alert for handling by the application.
<pre>
post(alert: <a href="#alertmessage">AlertMessage</a>): void
</pre>
### alert\$()
Observe alerts posted by other parts of the application.
<pre>
alert$(): <a href="#observable">Observable</a>&lt;<a href="#alertmessage">AlertMessage</a>&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AlertMessage
<pre>
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';
}
</pre>
Defined at
[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).
### 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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
+225
View File
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50).
The following Utility API implements this type:
[appThemeApiRef](./README.md#apptheme)
## Members
### getInstalledThemes()
Get a list of available themes.
<pre>
getInstalledThemes(): <a href="#apptheme">AppTheme</a>[]
</pre>
### activeThemeId\$()
Observe the currently selected theme. A value of undefined means no specific
theme has been selected.
<pre>
activeThemeId$(): <a href="#observable">Observable</a>&lt;string | undefined&gt;
</pre>
### getActiveThemeId()
Get the current theme ID. Returns undefined if no specific theme is selected.
<pre>
getActiveThemeId(): string | undefined
</pre>
### setActiveThemeId()
Set a specific theme to use in the app, overriding the default theme selection.
Clear the selection by passing in undefined.
<pre>
setActiveThemeId(themeId?: string): void
</pre>
## 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.
<pre>
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: <a href="#backstagetheme">BackstageTheme</a>;
}
</pre>
Defined at
[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).
### BackstagePalette
<pre>
export type BackstagePalette = Palette &amp; <a href="#paletteadditions">PaletteAdditions</a>
</pre>
Defined at
[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L63).
Referenced by: [BackstageTheme](#backstagetheme).
### BackstageTheme
<pre>
export interface BackstageTheme extends Theme {
palette: <a href="#backstagepalette">BackstagePalette</a>;
}
</pre>
Defined at
[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### PaletteAdditions
<pre>
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;
};
}
</pre>
Defined at
[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L23).
Referenced by: [BackstagePalette](#backstagepalette).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L144).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oktaAuthApiRef](./README.md#oktaauth)
## 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.
<pre>
getBackstageIdentity(
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
): Promise&lt;<a href="#backstageidentity">BackstageIdentity</a> | undefined&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
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;
}
</pre>
Defined at
[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).
### BackstageIdentity
<pre>
export type BackstageIdentity = {
/**
* The backstage user ID.
*/
id: string;
/**
* An ID token that can be used to authenticate the user within Backstage.
*/
idToken: string;
}
</pre>
Defined at
[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).
+179
View File
@@ -0,0 +1,179 @@
# Config
The Config type is defined at
[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#config)
## Members
### keys()
<pre>
keys(): string[]
</pre>
### get()
<pre>
get(key: string): <a href="#jsonvalue">JsonValue</a>
</pre>
### getOptional()
<pre>
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined
</pre>
### getConfig()
<pre>
getConfig(key: string): <a href="#config">Config</a>
</pre>
### getOptionalConfig()
<pre>
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined
</pre>
### getConfigArray()
<pre>
getConfigArray(key: string): <a href="#config">Config</a>[]
</pre>
### getOptionalConfigArray()
<pre>
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | undefined
</pre>
### getNumber()
<pre>
getNumber(key: string): number
</pre>
### getOptionalNumber()
<pre>
getOptionalNumber(key: string): number | undefined
</pre>
### getBoolean()
<pre>
getBoolean(key: string): boolean
</pre>
### getOptionalBoolean()
<pre>
getOptionalBoolean(key: string): boolean | undefined
</pre>
### getString()
<pre>
getString(key: string): string
</pre>
### getOptionalString()
<pre>
getOptionalString(key: string): string | undefined
</pre>
### getStringArray()
<pre>
getStringArray(key: string): string[]
</pre>
### getOptionalStringArray()
<pre>
getOptionalStringArray(key: string): string[] | undefined
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### Config
<pre>
export type Config = {
keys(): string[];
get(key: string): <a href="#jsonvalue">JsonValue</a>;
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined;
getConfig(key: string): Config;
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined;
getConfigArray(key: string): <a href="#config">Config</a>[];
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | 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;
}
</pre>
Defined at
[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),
[getOptionalConfigArray](#getoptionalconfigarray), [Config](#config).
### JsonArray
<pre>
export type JsonArray = <a href="#jsonvalue">JsonValue</a>[]
</pre>
Defined at
[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L18).
Referenced by: [JsonValue](#jsonvalue).
### JsonObject
<pre>
export type JsonObject = { [key in string]?: <a href="#jsonvalue">JsonValue</a> }
</pre>
Defined at
[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L17).
Referenced by: [JsonValue](#jsonvalue).
### JsonValue
<pre>
export type JsonValue =
| <a href="#jsonobject">JsonObject</a>
| <a href="#jsonarray">JsonArray</a>
| number
| string
| boolean
| null
</pre>
Defined at
[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).
+134
View File
@@ -0,0 +1,134 @@
# ErrorApi
The ErrorApi type is defined at
[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#error)
## Members
### post()
Post an error for handling by the application.
<pre>
post(error: <a href="#error">Error</a>, context?: <a href="#errorcontext">ErrorContext</a>): void
</pre>
### error\$()
Observe errors posted by other parts of the application.
<pre>
error$(): <a href="#observable">Observable</a>&lt;{ error: <a href="#error">Error</a>; context?: <a href="#errorcontext">ErrorContext</a> }&gt;
</pre>
## 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.
<pre>
type Error = {
name: string;
message: string;
stack?: string;
}
</pre>
Defined at
[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).
### ErrorContext
Provides additional information about an error that was posted to the
application.
<pre>
export type ErrorContext = {
// If set to true, this error should not be displayed to the user. Defaults to false.
hidden?: boolean;
}
</pre>
Defined at
[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).
### 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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41).
The following Utility API implements this type:
[featureFlagsApiRef](./README.md#featureflags)
## Members
### registeredFeatureFlags
Store a list of registered feature flags.
<pre>
registeredFeatureFlags: FeatureFlagsRegistryItem[]
</pre>
### getFlags()
Get a list of all feature flags from the current user.
<pre>
getFlags(): UserFlags
</pre>
### getRegisteredFlags()
Get a list of all registered flags.
<pre>
getRegisteredFlags(): FeatureFlagsRegistry
</pre>
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
The following Utility API implements this type:
[identityApiRef](./README.md#identity)
## 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.
<pre>
getUserId(): string
</pre>
### getProfile()
The profile of the signed in user.
<pre>
getProfile(): <a href="#profileinfo">ProfileInfo</a>
</pre>
### 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.
<pre>
getIdToken(): Promise&lt;string | undefined&gt;
</pre>
### logout()
Log out the current user
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### ProfileInfo
Profile information of the user.
<pre>
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;
}
</pre>
Defined at
[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).
+119
View File
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L67).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## 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.
<pre>
getAccessToken(
scope?: <a href="#oauthscope">OAuthScope</a>,
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
): Promise&lt;string&gt;
</pre>
### logout()
Log out the user's session. This will reload the page.
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
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;
}
</pre>
Defined at
[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).
### 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<OAuthApi & OpenIDConnectApi>({ ... })
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.
<pre>
export type OAuthScope = string | string[]
</pre>
Defined at
[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).
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
The following Utility API implements this type:
[oauthRequestApiRef](./README.md#oauthrequest)
## 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.
<pre>
createAuthRequester&lt;AuthResponse&gt;(
options: <a href="#authrequesteroptions">AuthRequesterOptions</a>&lt;AuthResponse&gt;,
): <a href="#authrequester">AuthRequester</a>&lt;AuthResponse&gt;
</pre>
### 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.
<pre>
authRequest$(): <a href="#observable">Observable</a>&lt;<a href="#pendingauthrequest">PendingAuthRequest</a>[]&gt;
</pre>
## 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.
<pre>
export type AuthProvider = {
/**
* Title for the auth provider, for example "GitHub"
*/
title: string;
/**
* Icon for the auth provider.
*/
icon: IconComponent;
}
</pre>
Defined at
[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).
### 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.
<pre>
export type AuthRequester&lt;AuthResponse&gt; = (
scopes: Set&lt;string&gt;,
) =&gt; Promise&lt;AuthResponse&gt;
</pre>
Defined at
[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).
### 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.
<pre>
export type AuthRequesterOptions&lt;AuthResponse&gt; = {
/**
* Information about the auth provider, which will be forwarded to auth requests.
*/
provider: <a href="#authprovider">AuthProvider</a>;
/**
* Implementation of the auth flow, which will be called synchronously when
* trigger() is called on an auth requests.
*/
onAuthRequest(scopes: Set&lt;string&gt;): Promise&lt;AuthResponse&gt;;
}
</pre>
Defined at
[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).
### 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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### 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.
<pre>
export type PendingAuthRequest = {
/**
* Information about the auth provider, as given in the AuthRequesterOptions
*/
provider: <a href="#authprovider">AuthProvider</a>;
/**
* Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
*/
reject: () =&gt; 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&lt;void&gt;;
}
</pre>
Defined at
[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).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L104).
The following Utility APIs implement this type:
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## 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.
<pre>
getIdToken(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise&lt;string&gt;
</pre>
### logout()
Log out the user's session. This will reload the page.
<pre>
logout(): Promise&lt;void&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
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;
}
</pre>
Defined at
[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).
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L127).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## 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.
<pre>
getProfile(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise&lt;<a href="#profileinfo">ProfileInfo</a> | undefined&gt;
</pre>
## Supporting types
These types are part of the API declaration, but may not be unique to this API.
### AuthRequestOptions
<pre>
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;
}
</pre>
Defined at
[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).
### ProfileInfo
Profile information of the user.
<pre>
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;
}
</pre>
Defined at
[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).
+139
View File
@@ -0,0 +1,139 @@
# 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.md)
ApiRef:
[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.md)
ApiRef:
[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.md)
ApiRef:
[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.md)
ApiRef:
[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.md)
ApiRef:
[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.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[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.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[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.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[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.md)
ApiRef:
[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.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md)
ApiRef:
[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.md)
ApiRef:
[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.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md),
[SessionStateApi](./SessionStateApi.md)
ApiRef:
[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.md)
ApiRef:
[storageApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L201).
The following Utility APIs implement this type:
- [githubAuthApiRef](./README.md#githubauth)
- [gitlabAuthApiRef](./README.md#gitlabauth)
- [googleAuthApiRef](./README.md#googleauth)
- [oauth2ApiRef](./README.md#oauth2)
- [oktaAuthApiRef](./README.md#oktaauth)
## Members
### sessionState\$()
<pre>
sessionState$(): <a href="#observable">Observable</a>&lt;<a href="#sessionstate">SessionState</a>&gt;
</pre>
## 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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### SessionState
Session state values passed to subscribers of the SessionStateApi.
<pre>
export enum SessionState {
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
}
</pre>
Defined at
[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).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
+186
View File
@@ -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/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
The following Utility API implements this type:
[storageApiRef](./README.md#storage)
## Members
### forBucket()
Create a bucket to store data in.
<pre>
forBucket(name: string): <a href="#storageapi">StorageApi</a>
</pre>
### get()
Get the current value for persistent data, use observe\$ to be notified of
updates.
<pre>
get&lt;T&gt;(key: string): T | undefined
</pre>
### remove()
Remove persistent data.
<pre>
remove(key: string): Promise&lt;void&gt;
</pre>
### set()
Save persistant data, and emit messages to anyone that is using observe\$ for
this key
<pre>
set(key: string, data: any): Promise&lt;void&gt;
</pre>
### observe\$()
Observe changes on a particular key in the bucket
<pre>
observe$&lt;T&gt;(key: string): <a href="#observable">Observable</a>&lt;<a href="#storagevaluechange">StorageValueChange</a>&lt;T&gt;&gt;
</pre>
## 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.
<pre>
export type Observable&lt;T&gt; = {
/**
* Subscribes to this observable to start receiving new values.
*/
subscribe(observer: <a href="#observer">Observer</a>&lt;T&gt;): <a href="#subscription">Subscription</a>;
subscribe(
onNext: (value: T) =&gt; void,
onError?: (error: Error) =&gt; void,
onComplete?: () =&gt; void,
): <a href="#subscription">Subscription</a>;
}
</pre>
Defined at
[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).
### Observer
This file contains non-react related core types used throught Backstage.
Observer interface for consuming an Observer, see TC39.
<pre>
export type Observer&lt;T&gt; = {
next?(value: T): void;
error?(error: Error): void;
complete?(): void;
}
</pre>
Defined at
[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).
### StorageApi
<pre>
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&lt;T&gt;(key: string): T | undefined;
/**
* Remove persistent data.
*
* @param {String} key Unique key associated with the data.
*/
remove(key: string): Promise&lt;void&gt;;
/**
* 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&lt;void&gt;;
/**
* Observe changes on a particular key in the bucket
* @param {String} key Unique key associated with the data
*/
observe$&lt;T&gt;(key: string): <a href="#observable">Observable</a>&lt;<a href="#storagevaluechange">StorageValueChange</a>&lt;T&gt;&gt;;
}
</pre>
Defined at
[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).
### StorageValueChange
<pre>
export type StorageValueChange&lt;T = any&gt; = {
key: string;
newValue?: T;
}
</pre>
Defined at
[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).
### Subscription
Subscription returned when subscribing to an Observable, see TC39.
<pre>
export type Subscription = {
/**
* Cancels the subscription
*/
unsubscribe(): void;
/**
* Value indicating whether the subscription is closed.
*/
readonly closed: Boolean;
}
</pre>
Defined at
[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).
+1
View File
@@ -15,6 +15,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 workspace example-app build && 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",
@@ -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<ConfigApi>({
id: 'core.config',
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
rules: {
'no-console': 0,
},
};
+21
View File
@@ -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.
+34
View File
@@ -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');
}
+51
View File
@@ -0,0 +1,51 @@
{
"name": "docgen",
"description": "Tool for generating API Documentation for itself",
"version": "0.1.1-alpha.16",
"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",
"docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github && prettier --write ../../docs/reference/utility-apis",
"clean": "backstage-cli clean",
"start": "nodemon --"
},
"bin": {
"backstage-docgen": "bin/backstage-docgen"
},
"dependencies": {
"chalk": "^4.0.0",
"commander": "^4.1.1",
"fs-extra": "^9.0.0",
"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"
},
"files": [
"bin",
"dist"
],
"nodemonConfig": {
"watch": "./src",
"exec": "bin/backstage-docgen",
"ext": "ts"
}
}
@@ -0,0 +1,267 @@
/*
* 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<MyApiType>({
id: 'my-id',
description: 'my-description',
});
`,
{
'/mem/type.ts': `export default class MyApi<T> {
constructor(private readonly info: { id: string, description: string }) {}
}`,
},
);
const typeLocator = TypeLocator.fromProgram(program, '/');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
const docGenerator = ApiDocGenerator.fromProgram(program, '/');
const doc = docGenerator.toDoc(apiInstance);
expect(doc.id).toBe('my-id');
expect(doc.description).toBe('my-description');
expect(doc.name).toBe('myApi');
expect(doc.file).toBe('mem/index.ts');
expect(doc.lineInFile).toBe(6);
expect(doc.interfaceInfos).toEqual([
{
dependentTypes: [],
docs: [],
file: 'mem/index.ts',
lineInFile: 4,
members: [],
name: 'MyApiType',
},
]);
});
it('should generate API docs', () => {
const program = createMemProgram(
`
import MyApi from './type';
/** MySubSubType Docs */
type MySubSubType = { n: number; };
/** MySubType Docs */
type MySubType = {
/** Field a docs */
a: boolean;
// Field b docs
b: MySubSubType;
}
/** MySecondSubType Docs */
/** With multiple comments */
export type MySecondSubType = { s: string };
// MyThirdSubType Docs that shouldn't show up
type MyThirdSubType = { b: boolean };
/** MyApiType Docs */
type MyApiType = {
/** Docs for x */
x: string;
// Line comments shouldn't show up
y: MySubType;
/** Multiple */
/** JsDoc */
/** Comments */
z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>;
};
/** Should not show up */
export const myApi = new MyApi<MyApiType>({
id: 'my-id',
description: 'my-description',
});
`,
{
'/mem/type.ts': `export default class MyApi<T> {
constructor(private readonly info: { id: string, description: string }) {}
}`,
},
);
const source = program.getSourceFile('/mem/index.ts');
// Figure out type IDs so we can make sure they match later
const checker = program.getTypeChecker();
const symbols = checker.getSymbolsInScope(
source!.getChildren().slice(-1)[0],
ts.SymbolFlags.TypeAlias,
);
const Ids = [
'MySubType',
'MySubSubType',
'MySecondSubType',
'MyThirdSubType',
].reduce((ids, name) => {
const symbol = symbols.find(s => s.escapedName === name)!;
const type = checker.getTypeAtLocation(symbol.declarations[0]);
ids[name] = (type.aliasSymbol as any).id;
return ids;
}, {} as { [key in string]: number });
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
const docGenerator = ApiDocGenerator.fromProgram(program, '/mem');
const doc = docGenerator.toDoc(apiInstance);
expect(doc.id).toBe('my-id');
expect(doc.description).toBe('my-description');
expect(doc.name).toBe('myApi');
expect(doc.file).toBe('index.ts');
expect(doc.interfaceInfos.length).toBe(1);
expect(doc.interfaceInfos[0].docs).toEqual(['MyApiType Docs']);
expect(doc.interfaceInfos[0].file).toBe('index.ts');
expect(doc.interfaceInfos[0].lineInFile).toBe(24);
expect(doc.interfaceInfos[0].name).toBe('MyApiType');
expect(doc.interfaceInfos[0].members).toEqual([
{
type: 'prop',
name: 'x',
path: 'MyApiType.x',
text: 'x: string',
docs: ['Docs for x'],
links: [],
},
{
type: 'prop',
name: 'y',
path: 'MyApiType.y',
text: 'y: MySubType',
docs: [],
links: [
{
id: Ids.MySubType,
name: 'MySubType',
path: 'index.ts/MySubType',
location: [3, 12],
},
],
},
{
type: 'method',
name: 'z',
path: 'MyApiType.z',
text:
'z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>',
docs: ['Multiple', 'JsDoc', 'Comments'],
links: [
{
id: Ids.MySecondSubType,
name: 'MySecondSubType',
path: 'index.ts/MySecondSubType',
location: [27, 42],
},
{
id: Ids.MyThirdSubType,
name: 'MyThirdSubType',
path: 'index.ts/MyThirdSubType',
location: [56, 70],
},
],
},
]);
expect(doc.interfaceInfos[0].dependentTypes).toEqual([
{
id: Ids.MySubType,
name: 'MySubType',
path: 'index.ts/MySubType',
file: 'index.ts',
lineInFile: 8,
text: `type MySubType = {
/** Field a docs */
a: boolean;
// Field b docs
b: MySubSubType;
}`,
docs: ['MySubType Docs'],
links: [
{
id: Ids.MySubSubType,
name: 'MySubSubType',
path: 'index.ts/MySubSubType',
location: [102, 114],
},
],
children: [],
},
{
id: Ids.MySubSubType,
name: 'MySubSubType',
path: 'index.ts/MySubSubType',
file: 'index.ts',
lineInFile: 5,
text: 'type MySubSubType = { n: number; }',
docs: ['MySubSubType Docs'],
links: [],
children: [],
},
{
id: Ids.MySecondSubType,
name: 'MySecondSubType',
path: 'index.ts/MySecondSubType',
file: 'index.ts',
lineInFile: 18,
text: 'export type MySecondSubType = { s: string }',
docs: ['MySecondSubType Docs', 'With multiple comments'],
links: [],
children: [],
},
{
id: Ids.MyThirdSubType,
name: 'MyThirdSubType',
path: 'index.ts/MyThirdSubType',
file: 'index.ts',
lineInFile: 21,
text: 'type MyThirdSubType = { b: boolean }',
docs: [],
links: [],
children: [],
},
]);
});
});
@@ -0,0 +1,334 @@
/*
* 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';
/**
* The ApiDocGenerator uses the typescript compiler API to build the data structure that
* describes a Backstage API and all of it's related types.
*
* It receives an exported instance that of the form
* `export name = createApiRef<Interface>({id: ..., description: ...})`.
* It will then traverse all fields and methods on the interface type, and also
* types and declaration of all types that are used by those, both directly and indirectly.
* While traversing, it collects information such as names, location in source, docs, etc.
*/
export default class ApiDocGenerator {
static fromProgram(program: ts.Program, sourcePath: string) {
return new ApiDocGenerator(program.getTypeChecker(), sourcePath);
}
constructor(
private readonly checker: ts.TypeChecker,
private readonly basePath: string,
) {}
/**
* Generate documentation information for a given exported symbol.
*/
toDoc(apiInstance: ExportedInstance): ApiDoc {
const { name, source, args, typeArgs } = apiInstance;
const [info] = args;
if (!ts.isObjectLiteralExpression(info)) {
throw new Error('api info is not an object literal');
}
const id = this.getObjectPropertyLiteral(info, 'id');
const description = this.getObjectPropertyLiteral(info, 'description');
const file = relative(this.basePath, source.fileName);
const { line } = source.getLineAndCharacterOfPosition(
apiInstance.node.getStart(),
);
const rootTypeNode = typeArgs[0];
const typeNodes = ts.isIntersectionTypeNode(rootTypeNode)
? rootTypeNode.types.slice()
: [rootTypeNode];
const interfaceInfos = typeNodes.map(typeNode =>
this.getInterfaceInfo(typeNode),
);
return {
id,
name,
file,
lineInFile: line + 1,
description,
interfaceInfos,
};
}
/**
* Grab jsDoc and regular comments for a node
*/
private getNodeDocs(node: ts.Node): string[] {
const docNodes = ((node && (node as any).jsDoc) || []) as ts.JSDoc[];
const docs = docNodes.map(docNode => docNode.comment || '').filter(Boolean);
return docs;
}
/**
* Collect information about a top-level type.
*/
private getInterfaceInfo(typeNode: ts.TypeNode): InterfaceInfo {
if (ts.isTypeQueryNode(typeNode)) {
throw new Error(
'APIs must have a proper type parameter, TypeQueries are now allowed',
);
}
if (!ts.isTypeReferenceNode(typeNode)) {
throw new Error('Interface is not a type node');
}
const type = this.checker.getTypeFromTypeNode(typeNode);
const interfaceMembers = type.symbol.members as Map<string, ts.Symbol>;
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 = Array.from(
interfaceMembers.values(),
).flatMap(fieldSymbol => this.getMemberInfo(name, fieldSymbol));
const members = membersAndTypes.map(t => t.member);
const dependentTypes = this.flattenTypes(
membersAndTypes.flatMap(t => t.dependentTypes),
);
return { name, docs, file, lineInFile: line + 1, members, dependentTypes };
}
/**
* Grab primitive values from an object literal expression.
*/
private getObjectPropertyLiteral(
objectLiteral: ts.ObjectLiteralExpression,
propertyName: string,
): string {
const matchingProp = objectLiteral.properties.filter(
prop =>
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
prop.name.text === propertyName,
)[0] as ts.PropertyAssignment;
if (!matchingProp) {
throw new Error(`no identifier found for property ${propertyName}`);
}
const { initializer } = matchingProp;
if (!ts.isStringLiteral(initializer)) {
throw new Error(`no string literal for ${propertyName}`);
}
return initializer.text;
}
/**
* Get field definitions for a given symbol.
*/
private getMemberInfo(
parentName: string,
symbol: ts.Symbol,
): { member: FieldInfo; dependentTypes: TypeInfo[] } {
const declaration = symbol.valueDeclaration;
const { links, infos: dependentTypes } = this.findAllTypeReferences(
declaration,
);
let type: FieldInfo['type'] = 'prop';
if (
ts.isPropertySignature(declaration) &&
declaration.type &&
ts.isFunctionTypeNode(declaration.type)
) {
type = 'method';
} else if (ts.isMethodSignature(declaration)) {
type = 'method';
}
return {
member: {
type,
name: symbol.name,
path: `${parentName}.${symbol.name}`,
text: declaration.getText().replace(/;$/, ''),
docs: this.getNodeDocs(declaration),
links: this.recalculateLinkOffsets(declaration, links),
},
dependentTypes,
};
}
/**
* Recursively search for references to types that we care about and want to include in the documentation.
*/
private findAllTypeReferences = (
node: ts.Node | undefined,
visited: Set<ts.Node> = 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<Array<MyType>>, since we're exiting on duplicate nodes, not types.
visited.add(node);
const children = node
.getChildren()
.flatMap(child => this.findAllTypeReferences(child, visited));
if (!ts.isTypeReferenceNode(node)) {
return {
links: children.flatMap(child => child.links),
infos: children.flatMap(child => child.infos),
};
}
const type = this.checker.getTypeFromTypeNode(node);
const info = this.validateTypeDeclaration(type);
if (!info) {
return {
links: children.flatMap(child => child.links),
infos: children.flatMap(child => child.infos),
};
}
const { symbol, declaration } = info;
const typeRefs = this.findAllTypeReferences(declaration, visited);
const sourceFile = declaration.getSourceFile();
const { line } = sourceFile.getLineAndCharacterOfPosition(
declaration.getStart(),
);
const file = 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.typeName.getStart(), node.typeName.getEnd()] as const,
};
return {
links: [link, ...children.flatMap(child => child.links)],
infos: [typeInfo, ...children.flatMap(child => child.infos)],
};
};
/**
* Check if a given type declaration is one that we care about.
*/
private validateTypeDeclaration(
type: ts.Type | undefined,
): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } {
if (!type) {
return undefined;
}
const symbol = type.aliasSymbol || type.symbol;
if (!symbol) {
return undefined;
}
const [declaration] = symbol.declarations;
// Don't generate standalone infos for type paramters
if (ts.isTypeParameterDeclaration(declaration)) {
return undefined;
}
if (declaration.getSourceFile().fileName.includes('node_modules')) {
return undefined;
}
return { symbol, declaration };
}
/**
* Flatten a tree of TypeInfos with children into a flat
* list of TypeInfos with duplicates removed.
*/
private flattenTypes(descs: TypeInfo[]): TypeInfo[] {
function getChildren(parent: TypeInfo): TypeInfo[] {
return [
{
...parent,
children: [],
},
...parent.children.flatMap(getChildren),
];
}
const flatDescs = descs.flatMap(getChildren);
const seenTypes = new Set<number>();
return flatDescs.filter(desc => {
if (seenTypes.has(desc.id)) {
return false;
}
seenTypes.add(desc.id);
return true;
});
}
/**
* Calculate link positions within a block of code, with 0 being
* the first character in the block.
*/
private recalculateLinkOffsets(
parent: ts.Node,
links: TypeLink[],
): TypeLink[] {
const parentStart = parent.getStart();
return links.map(link => {
const [start, end] = link.location;
return {
...link,
location: [start - parentStart, end - parentStart],
};
});
}
}
@@ -0,0 +1,163 @@
/*
* 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 { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types';
/**
* The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator,
* and prints it out as a markdown doc with custom code highlighting and links.
*/
export default class ApiDocPrinter {
printerFactory: () => MarkdownPrinter;
constructor(printerFactory: () => MarkdownPrinter) {
this.printerFactory = printerFactory;
}
/**
* Print an index file with all ApiRefs and what types they implement.
*/
printApiIndex(apiDocs: ApiDoc[]): Buffer {
const printer = this.printerFactory();
printer.header(1, 'Backstage Core Utility APIs');
printer.paragraph(
'The following is a list of all Utility APIs defined by `@backstage/core`.',
'They are available to use by plugins and components, and can be accessed ',
'using the `useApi` hook, also provided by `@backstage/core`.',
'For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.',
);
for (const api of apiDocs) {
printer.header(3, `${this.apiDisplayName(api)}`, api.id);
printer.paragraph(api.description);
const typeLinks = api.interfaceInfos.map(
i => `[${i.name}](${printer.pageLink(i.name)})`,
);
printer.paragraph(
`Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join(
', ',
)}`,
);
printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`);
}
return printer.toBuffer();
}
/**
* Print documentation page for a type implemented by an ApiRef and
*/
printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer {
const printer = this.printerFactory();
printer.header(1, apiType.name);
printer.paragraph(
`The ${apiType.name} type is defined at ${printer.srcLink(apiType)}.`,
);
const apiLinks = apiDocs
.filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name))
.map(ad => {
const link =
printer.indexLink() +
printer.headerLink(this.apiDisplayName(ad), ad.id);
return `[${ad.name}](${link})`;
});
if (apiLinks.length === 1) {
printer.paragraph(
`The following Utility API implements this type: ${apiLinks}`,
);
} else {
printer.paragraph(`The following Utility APIs implement this type:`);
for (const link of apiLinks) {
printer.text(` - ${link}`);
}
}
printer.header(2, 'Members');
this.addInterfaceMembers(printer, apiType);
if (apiType.dependentTypes.length) {
printer.header(2, 'Supporting types');
printer.paragraph(
'These types are part of the API declaration, but may not be unique to this API.',
);
this.addInterfaceTypes(printer, apiType);
}
return printer.toBuffer();
}
private addInterfaceMembers(
printer: MarkdownPrinter,
apiType: InterfaceInfo,
) {
for (const member of apiType.members) {
printer.header(
3,
`${member.name}${member.type === 'method' ? '()' : ''}`,
member.path,
);
for (const doc of member.docs) {
printer.text(doc);
}
printer.code(member);
}
}
private addInterfaceTypes(printer: MarkdownPrinter, apiType: InterfaceInfo) {
for (const type of apiType.dependentTypes
.slice()
.sort(sortSelector(x => x.name))) {
printer.header(3, `${type.name}`, type.path);
for (const doc of type.docs) {
printer.text(doc);
}
printer.code(type);
printer.paragraph(`Defined at ${printer.srcLink(type)}.`);
const usageLinks = [...apiType.members, ...apiType.dependentTypes]
.filter(member => {
return member.links.some(link => link.id === type.id);
})
.map(
({ name, path }) => `[${name}](${printer.headerLink(name, path)})`,
);
if (usageLinks.length) {
printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`);
}
}
}
private apiDisplayName(api: ApiDoc): string {
return api.name.replace(/ApiRef$/, '');
}
}
@@ -0,0 +1,145 @@
/*
* 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 GithubSlugger from 'github-slugger';
import sortSelector from './sortSelector';
import { MarkdownPrinter, TypeLink } from './types';
import { execSync } from 'child_process';
// TODO(Rugvip): provide through options?
const GH_BASE_URL = 'https://github.com/spotify/backstage';
const COMMIT_SHA =
process.env.COMMIT_SHA ||
execSync('git rev-parse HEAD').toString('utf8').trim();
/**
* The GithubMarkdownPrinter is a MarkdownPrinter for printing Github-flavored markdown documents.
*/
export default class GithubMarkdownPrinter implements MarkdownPrinter {
private str: string = '';
private line(line: string = '') {
this.str += `${line}\n`;
}
text(text: string) {
this.line(text);
this.line();
}
header(level: number, text: string) {
this.line(`${'#'.repeat(level)} ${text}`);
this.line();
}
headerLink(heading: string): string {
const slug = GithubSlugger.slug(heading);
return `#${slug}`;
}
indexLink() {
return './README.md';
}
pageLink(name: string) {
return `./${name}.md`;
}
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string {
const linkText = text ?? `${file}:${lineInFile}`;
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
return `[${linkText}](${href})`;
}
paragraph(...text: string[]) {
this.line(
text
.join('\n')
.trim()
.split('\n')
.map(line => line.trim())
.join('\n'),
);
this.line();
}
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
this.line('<pre>');
this.line(this.formatWithLinks({ text, links }));
this.line('</pre>');
this.line();
}
private escapeText: (text: string) => string = (() => {
const escapes: { [char in string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
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 `<a href="${link}">${this.escapeText(part.text)}</a>`;
}
return this.escapeText(part.text);
})
.join('');
}
toBuffer(): Buffer {
return Buffer.from(this.str, 'utf8');
}
}
@@ -0,0 +1,161 @@
/*
* 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, 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 TechdocsMarkdownPrinter is a MarkdownPrinter for building TechDocs markdown documents.
*/
export default class TechdocsMarkdownPrinter implements MarkdownPrinter {
private str: string = '';
private readonly highlighter: Highlighter;
constructor(highlighter: Highlighter) {
this.highlighter = highlighter;
// Remove line numbers from codeblocks
this.style('.linenodiv{ display: none }');
}
private line(line: string = '') {
this.str += `${line}\n`;
}
text(text: string) {
this.line(text);
this.line();
}
style(style: string) {
this.line('<style>');
this.line(style);
this.line('</style>');
}
header(level: number, text: string, id?: string) {
this.line(`${'#'.repeat(level)} ${text}${id ? ` {#${id}}` : ''}`);
this.line();
}
headerLink(heading: string, id?: string): string {
return `#${id ?? heading}`;
}
indexLink() {
return '../';
}
pageLink(name: string) {
return `./${name}/`;
}
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string {
const linkText = text ?? `${file}:${lineInFile}`;
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
return `[${linkText}](${href})`;
}
paragraph(...text: string[]) {
this.line(
text
.join('\n')
.trim()
.split('\n')
.map(line => line.trim())
.join('\n'),
);
this.line();
}
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
this.line('<div class="code">');
this.line(
'<div class="codehilite" style="background: #f0f0f0; padding: 0.225rem 0.6rem">',
);
this.line('<pre style="line-height: 125%">');
this.line(this.formatWithLinks({ text, links }));
this.line('</pre>');
this.line('</div>');
this.line('</div>');
}
private escapeText: (text: string) => string = (() => {
const escapes: { [char in string]: string } = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
})();
private formatWithLinks({
text,
links = [],
}: {
text: string;
links?: TypeLink[];
}): string {
const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
sortedLinks.reduce((lastEnd, link) => {
if (link.location[0] <= lastEnd) {
throw new Error(
`overlapping link detected for ${link.path}, ${link.location}`,
);
}
return link.location[1];
}, -1);
const parts: Array<{ text: string; path?: string }> = [];
const endLocation = sortedLinks.reduce((prev, link) => {
const [start, end] = link.location;
parts.push(
{ text: text.slice(prev, start) },
{ text: text.slice(start, end), path: link.path },
);
return end;
}, 0);
parts.push({ text: text.slice(endLocation) });
return parts
.map(part => {
if (part.path) {
const link = this.headerLink(part.text, part.path);
return `<a href="${link}">${this.escapeText(part.text)}</a>`;
}
return this.highlighter.highlight(this.escapeText(part.text));
})
.join('');
}
toBuffer(): Buffer {
return Buffer.from(this.str, 'utf8');
}
}
@@ -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<T> {}');
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const apiType = typeLocator.getExportedType('/mem/index.ts');
expect(apiType.symbol.name).toBe('default');
const [declaration] = apiType.symbol.declarations;
expect(declaration.kind).toBe(ts.SyntaxKind.ClassDeclaration);
expect((declaration as ts.ClassDeclaration).name).toBeDefined();
expect((declaration as ts.ClassDeclaration).name!.text).toBe('MyApi');
}, 10000);
it('should find api instance export', () => {
const program = createMemProgram(
`
import MyApi from './type';
type MyApiType = {};
export const myApi = new MyApi<MyApiType>();
`,
{
'/mem/type.ts': 'export default class MyApi<T> {}',
},
);
const typeLocator = TypeLocator.fromProgram(program, '/mem');
const { apiInstances } = typeLocator.findExportedInstances({
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
});
expect(apiInstances.length).toBe(1);
const [apiInstance] = apiInstances;
expect(apiInstance.name).toBe('myApi');
expect(apiInstance.source.fileName).toBe('/mem/index.ts');
expect(apiInstance.args).toEqual([]);
expect(apiInstance.typeArgs.length).toBe(1);
const [typeArg] = apiInstance.typeArgs;
expect(typeArg.kind).toBe(ts.SyntaxKind.TypeReference);
expect((typeArg as ts.TypeReferenceNode).typeName.getText()).toBe(
'MyApiType',
);
});
});
+142
View File
@@ -0,0 +1,142 @@
/*
* 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 {
static fromProgram(program: ts.Program, sourcePath: string) {
return new TypeLocator(program.getTypeChecker(), program, sourcePath);
}
constructor(
private readonly checker: ts.TypeChecker,
private readonly program: ts.Program,
private readonly sourcePath: string,
) {}
/**
* Get the type of a symbol by export path and name
*/
getExportedType(path: string, exportedName: string = 'default'): ts.Type {
const source = this.program.getSourceFile(path);
if (!source) {
throw new Error(`Source not found for path '${path}'`);
}
const exported = this.checker.getExportsOfModule(
this.checker.getSymbolAtLocation(source)!,
);
const [symbol] = exported.filter(e => e.name === exportedName);
if (!symbol) {
throw new Error(`No export '${exportedName}' found in ${path}`);
}
const type = this.checker.getTypeOfSymbolAtLocation(symbol, source);
return type;
}
/**
* Find exported instances and return values from calls using the types
* provided in the lookup table.
*/
findExportedInstances<T extends string>(
typeLookupTable: { [key in T]: ts.Type },
): { [key in T]: ExportedInstance[] } {
const docMap = new Map<ts.Type, ExportedInstance[]>();
for (const type of Object.values<ts.Type>(typeLookupTable)) {
docMap.set(type, []);
}
this.program.getSourceFiles().forEach(source => {
const inRoot = source.fileName.startsWith(this.sourcePath);
if (!inRoot) {
return;
}
ts.forEachChild(source, node => {
const decl = this.getExportedConstructorDeclaration(node);
if (!decl || !docMap.has(decl.constructorType)) {
return;
}
docMap.get(decl.constructorType)!.push({
node,
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.CallExpression | ts.NewExpression;
name: string;
}
| undefined {
if (!ts.isVariableStatement(node)) {
return undefined;
}
if (
!node.modifiers ||
!node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
) {
return undefined;
}
const { declarations } = node.declarationList;
if (declarations.length !== 1) {
return undefined;
}
const [declaration] = declarations;
const { initializer, name } = declaration;
if (!initializer || !name) {
return undefined;
}
if (!ts.isCallOrNewExpression(initializer)) {
return undefined;
}
if (!ts.isIdentifier(name)) {
return undefined;
}
const constructorType = this.checker.getTypeAtLocation(
initializer.expression,
);
return { constructorType, initializer, name: name.text };
}
}
@@ -0,0 +1,87 @@
/*
* 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';
// Simple syntax highlighter that mimics hilite
export default class TypescriptHighlighter implements Highlighter {
private static readonly basicTypes = [
'boolean',
'number',
'string',
'Array',
'object',
'Record',
'Set',
'Map',
'true',
'false',
'null',
'undefined',
'void',
'Promise',
'any',
'[0-9\\.]+',
];
// List of highlightings to apply, each with a match regex and the style that should be applied
private static readonly highlighters = [
[/(\/\*\*?(?:.|\n)+?\*\/)/g, 'color: #60a0b0; font-style: italic'], // block comment
[/(\/\/.*)/gm, 'color: #60a0b0; font-style: italic'], // line comment
[/('[^']*?'|"[^"]*?")/g, 'color: #4070a0'], // string literals
[
new RegExp(
`\\b(${TypescriptHighlighter.basicTypes.join('|')})\\b(?!:|,)`,
'g',
),
'color: #902000',
], // basic types
[
/^((?:export\s)?(?:type\s)?(?:interface\s)?)/g,
'color: #007020; font-weight: bold',
], // keywords
] as readonly [RegExp, string][];
highlight(fullText: string): string {
// Each part is either plain text that can be highlighted or text that is already highlighted
type HighlightPart = { text: string; highlighted?: boolean };
const painter = (regex: RegExp, style: string) => (part: HighlightPart) => {
if (part.highlighted) {
return [part];
}
// Apply highlighting to all matches of the regex by splitting into parts
return part.text.split(regex).map((text, index) => {
// Odd parts are the ones that matched the regex and should be highlighted.
if (index % 2 === 1) {
return {
text: `<span style="${style}">${text}</span>`,
highlighted: true,
};
}
return { text };
});
};
// Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted
return TypescriptHighlighter.highlighters
.reduce((parts, highlighter) => parts.flatMap(painter(...highlighter)), [
{ text: fullText },
])
.map(({ text }) => text)
.join('');
}
}
@@ -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],
]);
});
});
@@ -0,0 +1,33 @@
/*
* 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<T>(
selector: (x: T) => any,
): (a: T, b: T) => -1 | 1 | 0 {
return (a: T, b: T) => {
const aV = selector(a);
const bV = selector(b);
if (aV < bV) {
return -1;
} else if (aV > bV) {
return 1;
}
return 0;
};
}
+67
View File
@@ -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);
}
+119
View File
@@ -0,0 +1,119 @@
/*
* 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<FieldInfo>;
dependentTypes: TypeInfo[];
};
/**
* ApiDoc describes a documented API.
*/
export type ApiDoc = {
id: string;
name: string;
description: string;
file: string;
lineInFile: number;
interfaceInfos: InterfaceInfo[];
};
/**
* ExportedInstance describes an expression matching `export {name} = new {Contructor}<{typeArgs}...>({args}...)`
*/
export type ExportedInstance = {
node: ts.Node;
name: string;
source: ts.SourceFile;
args: Array<ts.Expression>;
typeArgs: Array<ts.TypeNode>;
};
export type Highlighter = {
highlight(test: string): string;
};
/**
* Markdown printer is an abstraction for printing markdown documents of different flavors.
*/
export type MarkdownPrinter = {
text(text: string): void;
header(level: number, text: string, id?: string): void;
paragraph(...text: string[]): void;
code(options: { text: string; links?: TypeLink[] }): void;
headerLink(header: string, id?: string): string;
/** Link from pages to index */
indexLink(): string;
/** Link from index to pages */
pageLink(name: string): string;
srcLink(
{ file, lineInFile }: { file: string; lineInFile: number },
text?: string,
): string;
toBuffer(): Buffer;
};
+126
View File
@@ -0,0 +1,126 @@
/*
* 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 fs from 'fs-extra';
import { resolve as resolvePath, join as joinPath } from 'path';
import ApiDocGenerator from './docgen/ApiDocGenerator';
import sortSelector from './docgen/sortSelector';
import TypeLocator from './docgen/TypeLocator';
import ApiDocPrinter from './docgen/ApiDocsPrinter';
import TypescriptHighlighter from './docgen/TypescriptHighlighter';
import GitHubMarkdownPrinter from './docgen/GitHubMarkdownPrinter';
import TechdocsMarkdownPrinter from './docgen/TechdocsMarkdownPrinter';
const FORMATS = ['github', 'techdocs'] as const;
export async function generate(
targetPath: string,
format: typeof FORMATS[number],
) {
if (!FORMATS.includes(format)) {
throw new TypeError(
`Invalid format, '${format}', must be one of ${FORMATS.join(', ')}`,
);
}
const rootDir = resolvePath(__dirname, '../../..');
const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src');
const targetDir = resolvePath(targetPath);
const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json'));
delete options.moduleResolution;
options.noEmit = true;
const program = ts.createProgram([resolvePath(srcDir, 'index.ts')], options);
const typeLocator = TypeLocator.fromProgram(program, srcDir);
const { apis } = typeLocator.findExportedInstances({
apis: typeLocator.getExportedType(
resolvePath(srcDir, 'index.ts'),
'createApiRef',
),
});
const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir);
const apiDocs = apis
.map(api => {
try {
return apiDocGenerator.toDoc(api);
} catch (error) {
throw new Error(
`Doc generation failed for API in ${api.source.fileName}, ${error.stack}`,
);
}
})
.sort(sortSelector(x => x.name));
const apiTypes = Object.values(
Object.fromEntries(
apiDocs.flatMap(d => d.interfaceInfos).map(i => [i.name, i]),
),
).sort(sortSelector(i => i.name));
if (format === 'techdocs') {
const docsDir = resolvePath(targetDir, 'docs');
await fs.ensureDir(docsDir);
const apiDocPrinter = new ApiDocPrinter(
() => new TechdocsMarkdownPrinter(new TypescriptHighlighter()),
);
await fs.writeFile(
joinPath(docsDir, 'README.md'),
apiDocPrinter.printApiIndex(apiDocs),
);
for (const apiType of Object.values(apiTypes)) {
const data = apiDocPrinter.printInterface(apiType, apiDocs);
await fs.writeFile(joinPath(docsDir, `${apiType.name}.md`), data);
}
await fs.writeFile(
resolvePath(targetDir, 'mkdocs.yml'),
[
'site_name: Backstage Core Utility API References',
'nav:',
` - API Index: 'README.md'`,
...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`),
'plugins:',
' - techdocs-core',
].join('\n'),
'utf8',
);
} else {
await fs.ensureDir(targetDir);
const apiDocPrinter = new ApiDocPrinter(() => new GitHubMarkdownPrinter());
await fs.writeFile(
joinPath(targetDir, 'README.md'),
apiDocPrinter.printApiIndex(apiDocs),
);
for (const apiType of Object.values(apiTypes)) {
const data = apiDocPrinter.printInterface(apiType, apiDocs);
await fs.writeFile(joinPath(targetDir, `${apiType.name}.md`), data);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* 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 program from 'commander';
import { resolve as resolvePath } from 'path';
import chalk from 'chalk';
import fs from 'fs-extra';
import { generate } from './generate';
const main = (argv: string[]) => {
const pkgJson = fs.readJsonSync(resolvePath(__dirname, '../package.json'));
program.name('docgen').version(pkgJson.version);
program
.command('generate')
.description(
'Generate documentation for the declarations in the core-api package',
)
.option('--output <output>', 'Output directory [./dist]')
.option(
'--format <format>',
'Output format, either techdocs or github [techdocs]',
)
.action(async cmd => {
await generate(cmd.output ?? './dist', cmd.format ?? 'techdocs');
});
program.on('command:*', () => {
console.log();
console.log(
chalk.red(`Invalid command: ${chalk.cyan(program.args.join(' '))}`),
);
console.log(chalk.red('See --help for a list of available commands.'));
console.log();
process.exit(1);
});
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);
+17
View File
@@ -3794,6 +3794,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"
@@ -8441,6 +8446,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"
@@ -10035,6 +10045,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"