+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AlertMessage
+
+
+export type AlertMessage = {
+ message: string;
+ // Severity will default to success since that is what material ui defaults the value to.
+ severity?: 'success' | 'info' | 'warning' | 'error';
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/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.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/docs/reference/utility-apis/AppThemeApi.md b/docs/reference/utility-apis/AppThemeApi.md
new file mode 100644
index 0000000000..02ab83afff
--- /dev/null
+++ b/docs/reference/utility-apis/AppThemeApi.md
@@ -0,0 +1,225 @@
+# AppThemeApi
+
+The AppThemeApi type is defined at
+[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/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.
+
+
+
+### getActiveThemeId()
+
+Get the current theme ID. Returns undefined if no specific theme is selected.
+
+
+getActiveThemeId(): string | undefined
+
+
+### setActiveThemeId()
+
+Set a specific theme to use in the app, overriding the default theme selection.
+
+Clear the selection by passing in undefined.
+
+
+setActiveThemeId(themeId?: string): void
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AppTheme
+
+Describes a theme provided by the app.
+
+
+export type AppTheme = {
+ /**
+ * ID used to remember theme selections.
+ */
+ id: string;
+
+ /**
+ * Title of the theme
+ */
+ title: string;
+
+ /**
+ * Theme variant
+ */
+ variant: 'light' | 'dark';
+
+ /**
+ * The specialized MaterialUI theme instance.
+ */
+ theme: BackstageTheme;
+}
+
+
+Defined at
+[packages/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.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/docs/reference/utility-apis/BackstageIdentityApi.md b/docs/reference/utility-apis/BackstageIdentityApi.md
new file mode 100644
index 0000000000..a9e0203403
--- /dev/null
+++ b/docs/reference/utility-apis/BackstageIdentityApi.md
@@ -0,0 +1,88 @@
+# BackstageIdentityApi
+
+The BackstageIdentityApi type is defined at
+[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/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.
+
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AuthRequestOptions
+
+
+export type AuthRequestOptions = {
+ /**
+ * If this is set to true, the user will not be prompted to log in,
+ * and an empty response will be returned if there is no existing session.
+ *
+ * This can be used to perform a check whether the user is logged in, or if you don't
+ * want to force a user to be logged in, but provide functionality if they already are.
+ *
+ * @default false
+ */
+ optional?: boolean;
+
+ /**
+ * If this is set to true, the request will bypass the regular oauth login modal
+ * and open the login popup directly.
+ *
+ * The method must be called synchronously from a user action for this to work in all browsers.
+ *
+ * @default false
+ */
+ instantPopup?: boolean;
+}
+
+export type BackstageIdentity = {
+ /**
+ * The backstage user ID.
+ */
+ id: string;
+
+ /**
+ * An ID token that can be used to authenticate the user within Backstage.
+ */
+ idToken: string;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L157).
+
+Referenced by: [getBackstageIdentity](#getbackstageidentity).
diff --git a/docs/reference/utility-apis/Config.md b/docs/reference/utility-apis/Config.md
new file mode 100644
index 0000000000..bca590bc9b
--- /dev/null
+++ b/docs/reference/utility-apis/Config.md
@@ -0,0 +1,179 @@
+# Config
+
+The Config type is defined at
+[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32).
+
+The following Utility API implements this type:
+[configApiRef](./README.md#config)
+
+## Members
+
+### keys()
+
+
+
+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).
diff --git a/docs/reference/utility-apis/ErrorApi.md b/docs/reference/utility-apis/ErrorApi.md
new file mode 100644
index 0000000000..10ae47e6df
--- /dev/null
+++ b/docs/reference/utility-apis/ErrorApi.md
@@ -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.
+
+
+
+## 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.
+
+
+
+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.
+
+
+export type ErrorContext = {
+ // If set to true, this error should not be displayed to the user. Defaults to false.
+ hidden?: boolean;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/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.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/docs/reference/utility-apis/FeatureFlagsApi.md b/docs/reference/utility-apis/FeatureFlagsApi.md
new file mode 100644
index 0000000000..5069c9272c
--- /dev/null
+++ b/docs/reference/utility-apis/FeatureFlagsApi.md
@@ -0,0 +1,33 @@
+# FeatureFlagsApi
+
+The FeatureFlagsApi type is defined at
+[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/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.
+
+
+
+### getFlags()
+
+Get a list of all feature flags from the current user.
+
+
+getFlags(): UserFlags
+
+
+### getRegisteredFlags()
+
+Get a list of all registered flags.
+
+
+getRegisteredFlags(): FeatureFlagsRegistry
+
diff --git a/docs/reference/utility-apis/IdentityApi.md b/docs/reference/utility-apis/IdentityApi.md
new file mode 100644
index 0000000000..2c94b7fcd0
--- /dev/null
+++ b/docs/reference/utility-apis/IdentityApi.md
@@ -0,0 +1,81 @@
+# IdentityApi
+
+The IdentityApi type is defined at
+[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/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.
+
+
+getUserId(): string
+
+
+### getProfile()
+
+The profile of the signed in user.
+
+
+
+### getIdToken()
+
+An OpenID Connect ID Token which proves the identity of the signed in user.
+
+The ID token will be undefined if the signed in user does not have a verified
+identity, such as a demo user or mocked user for e2e tests.
+
+
+getIdToken(): Promise<string | undefined>
+
+
+### logout()
+
+Log out the current user
+
+
+logout(): Promise<void>
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### ProfileInfo
+
+Profile information of the user.
+
+
+export type ProfileInfo = {
+ /**
+ * Email ID.
+ */
+ email?: string;
+
+ /**
+ * Display name that can be presented to the user.
+ */
+ displayName?: string;
+
+ /**
+ * URL to an avatar image of the user.
+ */
+ picture?: string;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
+
+Referenced by: [getProfile](#getprofile).
diff --git a/docs/reference/utility-apis/OAuthApi.md b/docs/reference/utility-apis/OAuthApi.md
new file mode 100644
index 0000000000..09a4c4b1b4
--- /dev/null
+++ b/docs/reference/utility-apis/OAuthApi.md
@@ -0,0 +1,119 @@
+# OAuthApi
+
+The OAuthApi type is defined at
+[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/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.
+
+
+
+### logout()
+
+Log out the user's session. This will reload the page.
+
+
+logout(): Promise<void>
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AuthRequestOptions
+
+
+export type AuthRequestOptions = {
+ /**
+ * If this is set to true, the user will not be prompted to log in,
+ * and an empty response will be returned if there is no existing session.
+ *
+ * This can be used to perform a check whether the user is logged in, or if you don't
+ * want to force a user to be logged in, but provide functionality if they already are.
+ *
+ * @default false
+ */
+ optional?: boolean;
+
+ /**
+ * If this is set to true, the request will bypass the regular oauth login modal
+ * and open the login popup directly.
+ *
+ * The method must be called synchronously from a user action for this to work in all browsers.
+ *
+ * @default false
+ */
+ instantPopup?: boolean;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/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({ ... })
+
+An array of scopes, or a scope string formatted according to the auth provider,
+which is typically a space separated list.
+
+See the documentation for each auth provider for the list of scopes supported by
+each provider.
+
+
+export type OAuthScope = string | string[]
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L38).
+
+Referenced by: [getAccessToken](#getaccesstoken).
diff --git a/docs/reference/utility-apis/OAuthRequestApi.md b/docs/reference/utility-apis/OAuthRequestApi.md
new file mode 100644
index 0000000000..fc38ca827b
--- /dev/null
+++ b/docs/reference/utility-apis/OAuthRequestApi.md
@@ -0,0 +1,232 @@
+# OAuthRequestApi
+
+The OAuthRequestApi type is defined at
+[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/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.
+
+
+
+### 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.
+
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AuthProvider
+
+Information about the auth provider that we're requesting a login towards.
+
+This should be shown to the user so that they can be informed about what login
+is being requested before a popup is shown.
+
+
+export type AuthProvider = {
+ /**
+ * Title for the auth provider, for example "GitHub"
+ */
+ title: string;
+
+ /**
+ * Icon for the auth provider.
+ */
+ icon: IconComponent;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+export type AuthRequesterOptions<AuthResponse> = {
+ /**
+ * Information about the auth provider, which will be forwarded to auth requests.
+ */
+ provider: AuthProvider;
+
+ /**
+ * Implementation of the auth flow, which will be called synchronously when
+ * trigger() is called on an auth requests.
+ */
+ onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/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.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+export type PendingAuthRequest = {
+ /**
+ * Information about the auth provider, as given in the AuthRequesterOptions
+ */
+ provider: AuthProvider;
+
+ /**
+ * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
+ */
+ reject: () => void;
+
+ /**
+ * Trigger the auth request to continue the auth flow, by for example showing a popup.
+ *
+ * Synchronously calls onAuthRequest with all scope currently in the request.
+ */
+ trigger(): Promise<void>;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
+
+Referenced by: [authRequest\$](#authrequest).
+
+### Subscription
+
+Subscription returned when subscribing to an Observable, see TC39.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/docs/reference/utility-apis/OpenIdConnectApi.md b/docs/reference/utility-apis/OpenIdConnectApi.md
new file mode 100644
index 0000000000..ecbf439b84
--- /dev/null
+++ b/docs/reference/utility-apis/OpenIdConnectApi.md
@@ -0,0 +1,75 @@
+# OpenIdConnectApi
+
+The OpenIdConnectApi type is defined at
+[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/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.
+
+
+
+### logout()
+
+Log out the user's session. This will reload the page.
+
+
+logout(): Promise<void>
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AuthRequestOptions
+
+
+export type AuthRequestOptions = {
+ /**
+ * If this is set to true, the user will not be prompted to log in,
+ * and an empty response will be returned if there is no existing session.
+ *
+ * This can be used to perform a check whether the user is logged in, or if you don't
+ * want to force a user to be logged in, but provide functionality if they already are.
+ *
+ * @default false
+ */
+ optional?: boolean;
+
+ /**
+ * If this is set to true, the request will bypass the regular oauth login modal
+ * and open the login popup directly.
+ *
+ * The method must be called synchronously from a user action for this to work in all browsers.
+ *
+ * @default false
+ */
+ instantPopup?: boolean;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
+
+Referenced by: [getIdToken](#getidtoken).
diff --git a/docs/reference/utility-apis/ProfileInfoApi.md b/docs/reference/utility-apis/ProfileInfoApi.md
new file mode 100644
index 0000000000..469c923cb0
--- /dev/null
+++ b/docs/reference/utility-apis/ProfileInfoApi.md
@@ -0,0 +1,94 @@
+# ProfileInfoApi
+
+The ProfileInfoApi type is defined at
+[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/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.
+
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### AuthRequestOptions
+
+
+export type AuthRequestOptions = {
+ /**
+ * If this is set to true, the user will not be prompted to log in,
+ * and an empty response will be returned if there is no existing session.
+ *
+ * This can be used to perform a check whether the user is logged in, or if you don't
+ * want to force a user to be logged in, but provide functionality if they already are.
+ *
+ * @default false
+ */
+ optional?: boolean;
+
+ /**
+ * If this is set to true, the request will bypass the regular oauth login modal
+ * and open the login popup directly.
+ *
+ * The method must be called synchronously from a user action for this to work in all browsers.
+ *
+ * @default false
+ */
+ instantPopup?: boolean;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
+
+Referenced by: [getProfile](#getprofile).
+
+### ProfileInfo
+
+Profile information of the user.
+
+
+export type ProfileInfo = {
+ /**
+ * Email ID.
+ */
+ email?: string;
+
+ /**
+ * Display name that can be presented to the user.
+ */
+ displayName?: string;
+
+ /**
+ * URL to an avatar image of the user.
+ */
+ picture?: string;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
+
+Referenced by: [getProfile](#getprofile).
diff --git a/docs/reference/utility-apis/README.md b/docs/reference/utility-apis/README.md
new file mode 100644
index 0000000000..66da8403b0
--- /dev/null
+++ b/docs/reference/utility-apis/README.md
@@ -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)
diff --git a/docs/reference/utility-apis/SessionStateApi.md b/docs/reference/utility-apis/SessionStateApi.md
new file mode 100644
index 0000000000..7ed67cbc62
--- /dev/null
+++ b/docs/reference/utility-apis/SessionStateApi.md
@@ -0,0 +1,115 @@
+# SessionStateApi
+
+The SessionStateApi type is defined at
+[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/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\$()
+
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### Observable
+
+Observable sequence of values and errors, see TC39.
+
+https://github.com/tc39/proposal-observable
+
+This is used as a common return type for observable values and can be created
+using many different observable implementations, such as zen-observable or
+RxJS 5.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+
+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.
+
+
+
+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.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/docs/reference/utility-apis/StorageApi.md b/docs/reference/utility-apis/StorageApi.md
new file mode 100644
index 0000000000..1eadec1302
--- /dev/null
+++ b/docs/reference/utility-apis/StorageApi.md
@@ -0,0 +1,186 @@
+# StorageApi
+
+The StorageApi type is defined at
+[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/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.
+
+
+
+## Supporting types
+
+These types are part of the API declaration, but may not be unique to this API.
+
+### Observable
+
+Observable sequence of values and errors, see TC39.
+
+https://github.com/tc39/proposal-observable
+
+This is used as a common return type for observable values and can be created
+using many different observable implementations, such as zen-observable or
+RxJS 5.
+
+
+export type Observable<T> = {
+ /**
+ * Subscribes to this observable to start receiving new values.
+ */
+ subscribe(observer: Observer<T>): Subscription;
+ subscribe(
+ onNext: (value: T) => void,
+ onError?: (error: Error) => void,
+ onComplete?: () => void,
+ ): Subscription;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/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.
+
+
+export interface StorageApi {
+ /**
+ * Create a bucket to store data in.
+ * @param {String} name Namespace for the storage to be stored under,
+ * will inherit previous namespaces too
+ */
+ forBucket(name: string): StorageApi;
+
+ /**
+ * Get the current value for persistent data, use observe$ to be notified of updates.
+ *
+ * @param {String} key Unique key associated with the data.
+ * @return {Object} data The data that should is stored.
+ */
+ get<T>(key: string): T | undefined;
+
+ /**
+ * Remove persistent data.
+ *
+ * @param {String} key Unique key associated with the data.
+ */
+ remove(key: string): Promise<void>;
+
+ /**
+ * Save persistant data, and emit messages to anyone that is using observe$ for this key
+ *
+ * @param {String} key Unique key associated with the data.
+ */
+ set(key: string, data: any): Promise<void>;
+
+ /**
+ * Observe changes on a particular key in the bucket
+ * @param {String} key Unique key associated with the data
+ */
+ observe$<T>(key: string): Observable<StorageValueChange<T>>;
+}
+
+
+Defined at
+[packages/core-api/src/apis/definitions/StorageApi.ts: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.
+
+
+export type Subscription = {
+ /**
+ * Cancels the subscription
+ */
+ unsubscribe(): void;
+
+ /**
+ * Value indicating whether the subscription is closed.
+ */
+ readonly closed: Boolean;
+}
+
+
+Defined at
+[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
+
+Referenced by: [Observable](#observable).
diff --git a/install/kubernetes/app.yaml b/install/kubernetes/app.yaml
index ed4108f11b..ce284afd31 100644
--- a/install/kubernetes/app.yaml
+++ b/install/kubernetes/app.yaml
@@ -11,17 +11,17 @@ spec:
matchLabels:
app: backstage
component: frontend
- template:
- metadata:
- labels:
- app: backstage
- component: frontend
- spec:
- containers:
- - name: app
- image: spotify/backstage:latest
- imagePullPolicy: Always
- ports:
- - containerPort: 80
- name: app
- protocol: TCP
+ template:
+ metadata:
+ labels:
+ app: backstage
+ component: frontend
+ spec:
+ containers:
+ - name: app
+ image: spotify/backstage:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 80
+ name: app
+ protocol: TCP
diff --git a/install/kubernetes/backend.yaml b/install/kubernetes/backend.yaml
index 669426fecd..bc82731958 100644
--- a/install/kubernetes/backend.yaml
+++ b/install/kubernetes/backend.yaml
@@ -11,17 +11,17 @@ spec:
matchLabels:
app: backstage
component: backend
- template:
- metadata:
- labels:
- app: backstage
- component: backend
- spec:
- containers:
- - name: backend
- image: spotify/backstage-backend:latest
- imagePullPolicy: Always
- ports:
- - containerPort: 7000
- name: backend
- protocol: TCP
+ template:
+ metadata:
+ labels:
+ app: backstage
+ component: backend
+ spec:
+ containers:
+ - name: backend
+ image: spotify/backstage-backend:latest
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 7000
+ name: backend
+ protocol: TCP
diff --git a/package.json b/package.json
index 9d702c4755..254142a733 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,6 @@
},
"scripts": {
"start": "yarn workspace example-app start",
- "bundle": "yarn workspace example-app bundle",
"build": "lerna run build",
"tsc": "tsc",
"clean": "backstage-cli clean && lerna run clean",
@@ -16,7 +15,8 @@
"lint": "lerna run lint --since origin/master --",
"lint:all": "lerna run lint --",
"lint:type-deps": "node scripts/check-type-dependencies.js",
- "docker-build": "yarn bundle && docker build . -t spotify/backstage",
+ "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",
"remove-plugin": "backstage-cli remove-plugin",
diff --git a/packages/app/package.json b/packages/app/package.json
index 4dc2f9c4c1..ea4c308380 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -51,7 +51,7 @@
},
"scripts": {
"start": "backstage-cli app:serve",
- "bundle": "backstage-cli app:build",
+ "build": "backstage-cli app:build",
"clean": "backstage-cli clean",
"test": "backstage-cli test",
"test:e2e": "start-server-and-test start http://localhost:3000 cy:dev",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index c569308fc7..9e9ca1edd0 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -19,6 +19,8 @@ import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
+ useApi,
+ configApiRef,
} from '@backstage/core';
import React, { FC } from 'react';
import Root from './components/Root';
@@ -30,12 +32,13 @@ const app = createApp({
apis,
plugins: Object.values(plugins),
components: {
- SignInPage: props => (
-
- ),
+ SignInPage: props => {
+ const configApi = useApi(configApiRef);
+ const providersConfig = configApi.getOptionalConfig('auth.providers');
+ const providers = providersConfig?.keys() ?? [];
+
+ return ;
+ },
},
});
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 5f93574665..18131236df 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -29,12 +29,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"stoppable": "^1.1.0",
diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts
index e8a5a7f47a..a7a3d64bd1 100644
--- a/packages/backend-common/src/middleware/errorHandler.test.ts
+++ b/packages/backend-common/src/middleware/errorHandler.test.ts
@@ -96,4 +96,38 @@ describe('errorHandler', () => {
expect((await r.get('/NotFoundError')).status).toBe(404);
expect((await r.get('/ConflictError')).status).toBe(409);
});
+
+ it('logs all 500 errors', async () => {
+ const app = express();
+
+ const mockLogger = { child: jest.fn(), error: jest.fn() };
+ mockLogger.child.mockImplementation(() => mockLogger as any);
+
+ const thrownError = new Error('some error');
+
+ app.use('/breaks', () => {
+ throw thrownError;
+ });
+ app.use(errorHandler({ logger: mockLogger as any }));
+
+ await request(app).get('/breaks');
+
+ expect(mockLogger.error).toHaveBeenCalledWith(thrownError);
+ });
+
+ it('does not log 400 errors', async () => {
+ const app = express();
+
+ const mockLogger = { child: jest.fn(), error: jest.fn() };
+ mockLogger.child.mockImplementation(() => mockLogger as any);
+
+ app.use('/NotFound', () => {
+ throw new errors.NotFoundError();
+ });
+ app.use(errorHandler({ logger: mockLogger as any }));
+
+ await request(app).get('/NotFound');
+
+ expect(mockLogger.error).not.toHaveBeenCalled();
+ });
});
diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts
index ad8f170dcd..7365ce8b93 100644
--- a/packages/backend-common/src/middleware/errorHandler.ts
+++ b/packages/backend-common/src/middleware/errorHandler.ts
@@ -15,7 +15,9 @@
*/
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
+import { Logger } from 'winston';
import * as errors from '../errors';
+import { getRootLogger } from '../logging';
export type ErrorHandlerOptions = {
/**
@@ -24,6 +26,13 @@ export type ErrorHandlerOptions = {
* If not specified, by default shows stack traces only in development mode.
*/
showStackTraces?: boolean;
+
+ /**
+ * Logger instance to log any 5xx errors.
+ *
+ * If not specified, the root logger will be used.
+ */
+ logger?: Logger;
};
/**
@@ -39,12 +48,17 @@ export type ErrorHandlerOptions = {
*
* @returns An Express error request handler
*/
+
export function errorHandler(
options: ErrorHandlerOptions = {},
): ErrorRequestHandler {
const showStackTraces =
options.showStackTraces ?? process.env.NODE_ENV === 'development';
+ const logger = (options.logger || getRootLogger()).child({
+ type: 'errorHandler',
+ });
+
/* eslint-disable @typescript-eslint/no-unused-vars */
return (
error: Error,
@@ -61,6 +75,11 @@ export function errorHandler(
const status = getStatusCode(error);
const message = showStackTraces ? error.stack : error.message;
+
+ if (logger && status >= 500) {
+ logger.error(error);
+ }
+
response.status(status).send(message);
};
}
diff --git a/packages/backend-common/src/middleware/index.ts b/packages/backend-common/src/middleware/index.ts
index 083b36c3e9..76c52d8830 100644
--- a/packages/backend-common/src/middleware/index.ts
+++ b/packages/backend-common/src/middleware/index.ts
@@ -17,3 +17,4 @@
export * from './errorHandler';
export * from './notFoundHandler';
export * from './requestLoggingHandler';
+export * from './statusCheckHandler';
diff --git a/packages/backend-common/src/middleware/statusCheckHandler.test.ts b/packages/backend-common/src/middleware/statusCheckHandler.test.ts
new file mode 100644
index 0000000000..7ed1a65b58
--- /dev/null
+++ b/packages/backend-common/src/middleware/statusCheckHandler.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import express from 'express';
+import request from 'supertest';
+import { statusCheckHandler } from './statusCheckHandler';
+
+describe('statusCheckHandler', () => {
+ it('gives status 200 when using default', async () => {
+ const app = express();
+ app.use('/healthcheck', await statusCheckHandler());
+
+ const response = await request(app).get('/healthcheck');
+
+ expect(response.status).toBe(200);
+ expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
+ });
+
+ it('gives status 200 when status function returns true', async () => {
+ const app = express();
+ const status = { foo: 'bar' };
+ const statusCheck = () => Promise.resolve(status);
+ app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
+
+ const response = await request(app).get('/healthcheck');
+
+ expect(response.status).toBe(200);
+ expect(response.text).toBe(JSON.stringify(status));
+ });
+
+ it('gives status 500 when status check throws an error', async () => {
+ const app = express();
+ const statusCheck = () => {
+ throw Error('error!');
+ };
+ app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
+
+ const response = await request(app).get('/healthcheck');
+
+ expect(response.status).toBe(500);
+ });
+});
diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/middleware/statusCheckHandler.ts
new file mode 100644
index 0000000000..3f62f04f59
--- /dev/null
+++ b/packages/backend-common/src/middleware/statusCheckHandler.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NextFunction, Request, Response, RequestHandler } from 'express';
+
+export type StatusCheck = () => Promise;
+
+export interface StatusCheckHandlerOptions {
+ /**
+ * Optional status function which returns a message.
+ */
+ statusCheck?: StatusCheck;
+}
+
+/**
+ * Express middleware for status checks.
+ *
+ * This is commonly used to implement healthcheck and readiness routes.
+ *
+ * @param options An optional configuration object.
+ * @returns An Express error request handler
+ */
+export async function statusCheckHandler(
+ options: StatusCheckHandlerOptions = {},
+): Promise {
+ const statusCheck: StatusCheck = options.statusCheck
+ ? options.statusCheck
+ : () => Promise.resolve({ status: 'ok' });
+
+ return async (_request: Request, response: Response, next: NextFunction) => {
+ try {
+ const status = await statusCheck();
+ response.status(200).header('').send(status);
+ } catch (err) {
+ next(err);
+ }
+ };
+}
diff --git a/packages/backend-common/src/service/createStatusCheckRouter.test.ts b/packages/backend-common/src/service/createStatusCheckRouter.test.ts
new file mode 100644
index 0000000000..6c15e7b5f9
--- /dev/null
+++ b/packages/backend-common/src/service/createStatusCheckRouter.test.ts
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import express from 'express';
+import * as winston from 'winston';
+import request from 'supertest';
+import { createStatusCheckRouter } from './createStatusCheckRouter';
+
+describe('createStatusCheckRouter', () => {
+ const logger = winston.createLogger();
+
+ it('gives status 200 when using default path', async () => {
+ const app = express();
+ app.use('', await createStatusCheckRouter({ logger }));
+
+ const response = await request(app).get('/healthcheck');
+
+ expect(response.status).toBe(200);
+ expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
+ });
+
+ it('gives status 200 when using custom path', async () => {
+ const app = express();
+ app.use('', await createStatusCheckRouter({ logger, path: '/ready' }));
+
+ const response = await request(app).get('/ready');
+
+ expect(response.status).toBe(200);
+ expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
+ });
+
+ it('gives status 500 when status check throws an error', async () => {
+ const app = express();
+ const statusCheck = () => {
+ throw Error('error!');
+ };
+ app.use('', await createStatusCheckRouter({ logger, statusCheck }));
+
+ const response = await request(app).get('/healthcheck');
+
+ expect(response.status).toBe(500);
+ });
+});
diff --git a/packages/backend-common/src/service/createStatusCheckRouter.ts b/packages/backend-common/src/service/createStatusCheckRouter.ts
new file mode 100644
index 0000000000..c6014cbfc2
--- /dev/null
+++ b/packages/backend-common/src/service/createStatusCheckRouter.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Logger } from 'winston';
+import Router from 'express-promise-router';
+import express from 'express';
+import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
+
+export interface StatusCheckRouterOptions {
+ logger: Logger;
+ path?: string;
+ statusCheck?: StatusCheck;
+}
+
+export async function createStatusCheckRouter(
+ options: StatusCheckRouterOptions,
+): Promise {
+ const router = Router();
+ const { path = '/healthcheck', statusCheck } = options;
+
+ router.use(path, await statusCheckHandler({ statusCheck }));
+ router.use(errorHandler());
+
+ return router;
+}
diff --git a/packages/backend-common/src/service/index.ts b/packages/backend-common/src/service/index.ts
index 9bba9d6baf..58e310032d 100644
--- a/packages/backend-common/src/service/index.ts
+++ b/packages/backend-common/src/service/index.ts
@@ -15,4 +15,5 @@
*/
export { createServiceBuilder } from './createServiceBuilder';
+export { createStatusCheckRouter } from './createStatusCheckRouter';
export type { ServiceBuilder } from './types';
diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts
index b39df27527..070794969f 100644
--- a/packages/backend-common/src/service/types.ts
+++ b/packages/backend-common/src/service/types.ts
@@ -16,7 +16,7 @@
import { ConfigReader } from '@backstage/config';
import cors from 'cors';
-import { Router } from 'express';
+import { Router, RequestHandler } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
@@ -64,7 +64,7 @@ export type ServiceBuilder = {
* @param root The root URL to bind to (e.g. "/api/function1")
* @param router An express router
*/
- addRouter(root: string, router: Router): ServiceBuilder;
+ addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
/**
* Starts the server using the given settings.
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 3a53287648..7ca11fc636 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -20,8 +20,8 @@
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.16",
"@backstage/catalog-model": "^0.1.1-alpha.16",
- "@backstage/config": "^0.1.1-alpha.13",
- "@backstage/config-loader": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
+ "@backstage/config-loader": "^0.1.1-alpha.16",
"@backstage/plugin-auth-backend": "^0.1.1-alpha.16",
"@backstage/plugin-catalog-backend": "^0.1.1-alpha.16",
"@backstage/plugin-identity-backend": "^0.1.1-alpha.16",
@@ -30,6 +30,7 @@
"@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.16",
"@backstage/plugin-sentry-backend": "^0.1.1-alpha.16",
"@backstage/plugin-techdocs-backend": "^0.1.1-alpha.16",
+ "@backstage/plugin-graphql-backend": "^0.1.1-alpha.16",
"@octokit/rest": "^18.0.0",
"dockerode": "^3.2.0",
"express": "^4.17.1",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 9d2d89a049..3fccfe9c59 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -30,6 +30,7 @@ import {
import { ConfigReader, AppConfig } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import knex, { PgConnectionConfig } from 'knex';
+import healthcheck from './plugins/healthcheck';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import identity from './plugins/identity';
@@ -38,6 +39,7 @@ import scaffolder from './plugins/scaffolder';
import sentry from './plugins/sentry';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
+import graphql from './plugins/graphql';
import { PluginEnvironment } from './types';
function makeCreateEnv(loadedConfigs: AppConfig[]) {
@@ -83,10 +85,11 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
- const configs = await loadConfig();
+ const configs = await loadConfig({ shouldReadSecrets: true });
const configReader = ConfigReader.fromConfigs(configs);
const createEnv = makeCreateEnv(configs);
+ const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
@@ -95,9 +98,11 @@ async function main() {
const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar'));
const sentryEnv = useHotMemoize(module, () => createEnv('sentry'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
+ const graphqlEnv = useHotMemoize(module, () => createEnv('graphql'));
const service = createServiceBuilder(module)
.loadConfig(configReader)
+ .addRouter('', await healthcheck(healthcheckEnv))
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/rollbar', await rollbar(rollbarEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
@@ -105,7 +110,8 @@ async function main() {
.addRouter('/auth', await auth(authEnv))
.addRouter('/identity', await identity(identityEnv))
.addRouter('/techdocs', await techdocs(techdocsEnv))
- .addRouter('/proxy', await proxy(proxyEnv));
+ .addRouter('/proxy', await proxy(proxyEnv))
+ .addRouter('/graphql', await graphql(graphqlEnv));
await service.start().catch(err => {
console.log(err);
diff --git a/packages/backend/src/plugins/graphql.ts b/packages/backend/src/plugins/graphql.ts
new file mode 100644
index 0000000000..0f9b167d19
--- /dev/null
+++ b/packages/backend/src/plugins/graphql.ts
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createRouter } from '@backstage/plugin-graphql-backend';
+import type { PluginEnvironment } from '../types';
+
+export default async function createPlugin({ logger }: PluginEnvironment) {
+ return await createRouter({
+ logger,
+ });
+}
diff --git a/packages/backend/src/plugins/healthcheck.ts b/packages/backend/src/plugins/healthcheck.ts
new file mode 100644
index 0000000000..3dd9a67827
--- /dev/null
+++ b/packages/backend/src/plugins/healthcheck.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createStatusCheckRouter } from '@backstage/backend-common';
+import { PluginEnvironment } from '../types';
+
+export default async function createRouter({ logger }: PluginEnvironment) {
+ return await createStatusCheckRouter({ logger, path: '/healthcheck' });
+}
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index 14212e84e0..43ac830327 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -20,7 +20,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"json-schema": "^0.2.5",
diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts
index 1763d256f5..28c1fde9e3 100644
--- a/packages/cli/asset-types/asset-types.d.ts
+++ b/packages/cli/asset-types/asset-types.d.ts
@@ -50,6 +50,11 @@ declare module '*.webp' {
export default src;
}
+declare module '*.yaml' {
+ const src: string;
+ export default src;
+}
+
declare module '*.icon.svg' {
import { ComponentType } from 'react';
import { SvgIconProps } from '@material-ui/core';
diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js
index 23312bd308..da9b3ec3e6 100644
--- a/packages/cli/config/jest.js
+++ b/packages/cli/config/jest.js
@@ -38,7 +38,7 @@ async function getConfig() {
transform: {
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
'\\.(js|jsx|ts|tsx)': require.resolve('ts-jest'),
- '\\.(bmp|gif|jpe|png|frag|xml|svg)': require.resolve(
+ '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)': require.resolve(
'./jestFileTransform.js',
),
},
@@ -49,7 +49,7 @@ async function getConfig() {
// Default behaviour is to not apply transforms for node_modules, but we still want
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
transformIgnorePatterns: [
- '/node_modules/(?!.*\\.(?:esm\\.js|bmp|gif|jpe|png|frag|xml|svg)$)',
+ '/node_modules/(?!.*\\.(?:esm\\.js|bmp|gif|jpg|jpeg|png|frag|xml|svg)$)',
],
};
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 8786229240..07954223b4 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -29,14 +29,15 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
- "@backstage/config-loader": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
+ "@backstage/config-loader": "^0.1.1-alpha.16",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^8.1.0",
+ "@rollup/plugin-yaml": "^2.1.1",
"@spotify/eslint-config": "^7.0.1",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "4.3.x",
diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts
index 5987290604..e62e1fc6cd 100644
--- a/packages/cli/src/lib/builder/config.ts
+++ b/packages/cli/src/lib/builder/config.ts
@@ -26,6 +26,7 @@ import imageFiles from 'rollup-plugin-image-files';
import svgr from '@svgr/rollup';
import dts from 'rollup-plugin-dts';
import json from '@rollup/plugin-json';
+import yaml from '@rollup/plugin-yaml';
import { RollupOptions, OutputOptions } from 'rollup';
import { BuildOptions, Output } from './types';
@@ -93,6 +94,7 @@ export const makeConfigs = async (
postcss(),
imageFiles({ exclude: '**/*.icon.svg' }),
json(),
+ yaml(),
svgr({
include: '**/*.icon.svg',
template: svgrTemplate,
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts
index 0eaac9ad2d..56150ad968 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/lib/bundler/config.ts
@@ -25,11 +25,6 @@ import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { BundlingOptions, BackendBundlingOptions } from './types';
-// import checkRequiredFiles from 'react-dev-utils/checkRequiredFiles';
-// import ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin';
-// import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware';
-// import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
-// import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
@@ -126,10 +121,10 @@ export function createConfig(
output: {
path: paths.targetDist,
publicPath: validBaseUrl.pathname,
- filename: isDev ? '[name].js' : '[name].[hash:8].js',
+ filename: isDev ? '[name].js' : 'static/[name].[hash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
- : '[name].[chunkhash:8].chunk.js',
+ : 'static/[name].[chunkhash:8].chunk.js',
},
plugins,
};
diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts
index 03168b376d..acd6a937d9 100644
--- a/packages/cli/src/lib/bundler/optimization.ts
+++ b/packages/cli/src/lib/bundler/optimization.ts
@@ -46,7 +46,7 @@ export const optimization = (
},
filename: isDev
? 'module-[name].js'
- : 'module-[name].[chunkhash:8].js',
+ : 'static/module-[name].[chunkhash:8].js',
priority: 10,
minSize: 100000,
minChunks: 1,
diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts
index b498482f8a..d8186dac66 100644
--- a/packages/cli/src/lib/bundler/transforms.ts
+++ b/packages/cli/src/lib/bundler/transforms.ts
@@ -79,7 +79,7 @@ export const transforms = (
loader: require.resolve('url-loader'),
options: {
limit: 10000,
- name: 'static/media/[name].[hash:8].[ext]',
+ name: 'static/[name].[hash:8].[ext]',
},
},
{
@@ -111,8 +111,8 @@ export const transforms = (
} else {
plugins.push(
new MiniCssExtractPlugin({
- filename: '[name].[contenthash:8].css',
- chunkFilename: '[name].[id].[contenthash:8].css',
+ filename: 'static/[name].[contenthash:8].css',
+ chunkFilename: 'static/[name].[id].[contenthash:8].css',
}),
);
}
diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts
index d3ba1f6b12..f4a2181b40 100644
--- a/packages/cli/src/lib/packager/index.ts
+++ b/packages/cli/src/lib/packager/index.ts
@@ -134,11 +134,15 @@ async function findTargetPackages(pkgNames: string[]): Promise {
throw new Error(`Package '${name}' not found`);
}
- const pkgDeps = Object.keys(node.pkg.dependencies);
- const localDeps: string[] = Array.from(node.localDependencies.keys());
- const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
+ // Don't include dependencies of packages that are marked as bundled
+ if (!node.pkg.get('bundled')) {
+ const pkgDeps = Object.keys(node.pkg.dependencies);
+ const localDeps: string[] = Array.from(node.localDependencies.keys());
+ const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
+
+ searchNames.push(...filteredDeps);
+ }
- searchNames.push(...filteredDeps);
targets.set(name, node.pkg);
}
diff --git a/packages/cli/src/lib/paths.ts b/packages/cli/src/lib/paths.ts
index be3f3dcee8..ab3578ca1c 100644
--- a/packages/cli/src/lib/paths.ts
+++ b/packages/cli/src/lib/paths.ts
@@ -47,7 +47,7 @@ export type Paths = {
resolveTargetRoot: ResolveFunc;
};
-// Looks for a package.json that has name: "root" to identify the root of the monorepo
+// Looks for a package.json with a workspace config to identify the root of the monorepo
export function findRootPath(topPath: string): string {
let path = topPath;
@@ -58,7 +58,7 @@ export function findRootPath(topPath: string): string {
if (exists) {
try {
const data = fs.readJsonSync(packagePath);
- if (data.name === 'root' || data.name.includes('backstage-e2e')) {
+ if (data.workspaces?.packages) {
return path;
}
} catch (error) {
@@ -70,9 +70,7 @@ export function findRootPath(topPath: string): string {
const newPath = dirname(path);
if (newPath === path) {
- throw new Error(
- `No package.json with name "root" found as a parent of ${topPath}`,
- );
+ return topPath; // We didn't find any root package.json, assume we're not in a monorepo
}
path = newPath;
}
diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts
index 8cb5a1e7df..94136f1dbc 100644
--- a/packages/cli/src/types.d.ts
+++ b/packages/cli/src/types.d.ts
@@ -27,3 +27,5 @@ declare module 'rollup-plugin-image-files' {
declare module '@svgr/rollup' {
export default function svgr(options?: any): any;
}
+
+declare module '@rollup/plugin-yaml';
diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs
index fb162e66d8..b59ca63fa3 100644
--- a/packages/cli/templates/default-app/package.json.hbs
+++ b/packages/cli/templates/default-app/package.json.hbs
@@ -7,7 +7,6 @@
},
"scripts": {
"start": "yarn workspace app start",
- "bundle": "yarn workspace app bundle",
"build": "lerna run build",
"tsc": "tsc",
"clean": "backstage-cli clean && lerna run clean",
diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs
index 26cce2c5e6..a34d25f216 100644
--- a/packages/cli/templates/default-app/packages/app/package.json.hbs
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -32,7 +32,7 @@
},
"scripts": {
"start": "backstage-cli app:serve",
- "bundle": "backstage-cli app:build",
+ "build": "backstage-cli app:build",
"test": "backstage-cli test",
"lint": "backstage-cli lint",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 25512529b8..72bfc0378b 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "0.1.1-alpha.13",
+ "version": "0.1.1-alpha.16",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
diff --git a/packages/config/package.json b/packages/config/package.json
index aac52ab023..a401bc9af5 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config",
"description": "Config API used by Backstage core, backend, and CLI",
- "version": "0.1.1-alpha.13",
+ "version": "0.1.1-alpha.16",
"private": false,
"publishConfig": {
"access": "public",
diff --git a/packages/core-api/package.json b/packages/core-api/package.json
index 0f0251f23c..38abdf9487 100644
--- a/packages/core-api/package.json
+++ b/packages/core-api/package.json
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
"@backstage/theme": "^0.1.1-alpha.16",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -42,7 +42,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.16",
- "@backstage/test-utils-core": "^0.1.1-alpha.13",
+ "@backstage/test-utils-core": "^0.1.1-alpha.16",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts
index 63a588fc10..1fa6e70d1f 100644
--- a/packages/core-api/src/apis/definitions/ConfigApi.ts
+++ b/packages/core-api/src/apis/definitions/ConfigApi.ts
@@ -17,7 +17,7 @@ import { createApiRef } from '../ApiRef';
import { Config } from '@backstage/config';
// Using interface to make the ConfigApi name show up in docs
-export interface ConfigApi extends Config {}
+export type ConfigApi = Config;
export const configApiRef = createApiRef({
id: 'core.config',
diff --git a/packages/core/package.json b/packages/core/package.json
index 20a947a202..4b29f22ba3 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -29,7 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.13",
+ "@backstage/config": "^0.1.1-alpha.16",
"@backstage/core-api": "^0.1.1-alpha.16",
"@backstage/theme": "^0.1.1-alpha.16",
"@material-ui/core": "^4.9.1",
diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx
index 5f66fc6558..e69d63186d 100644
--- a/packages/core/src/layout/Sidebar/UserSettings.tsx
+++ b/packages/core/src/layout/Sidebar/UserSettings.tsx
@@ -22,6 +22,7 @@ import {
oauth2ApiRef,
oktaAuthApiRef,
useApi,
+ configApiRef,
} from '@backstage/core-api';
import Collapse from '@material-ui/core/Collapse';
import SignOutIcon from '@material-ui/icons/MeetingRoom';
@@ -39,6 +40,9 @@ export function SidebarUserSettings() {
const { isOpen: sidebarOpen } = useContext(SidebarContext);
const [open, setOpen] = React.useState(false);
const identityApi = useApi(identityApiRef);
+ const configApi = useApi(configApiRef);
+ const providersConfig = configApi.getOptionalConfig('auth.providers');
+ const providers = providersConfig?.keys() ?? [];
// Close the provider list when sidebar collapse
useEffect(() => {
@@ -49,31 +53,41 @@ export function SidebarUserSettings() {
<>
-
-
-
-
-
+ {providers.includes('google') && (
+
+ )}
+ {providers.includes('github') && (
+
+ )}
+ {providers.includes('gitlab') && (
+
+ )}
+ {providers.includes('okta') && (
+
+ )}
+ {providers.includes('oauth2') && (
+
+ )}
{
+ it('should generate empty API doc', () => {
+ const program = createMemProgram(
+ `
+ import MyApi from './type';
+
+ type MyApiType = {};
+
+ export const myApi = new MyApi({
+ id: 'my-id',
+ description: 'my-description',
+ });
+ `,
+ {
+ '/mem/type.ts': `export default class MyApi {
+ constructor(private readonly info: { id: string, description: string }) {}
+ }`,
+ },
+ );
+
+ const typeLocator = TypeLocator.fromProgram(program, '/');
+
+ const { apiInstances } = typeLocator.findExportedInstances({
+ apiInstances: typeLocator.getExportedType('/mem/type.ts'),
+ });
+
+ expect(apiInstances.length).toBe(1);
+ const [apiInstance] = apiInstances;
+
+ const docGenerator = ApiDocGenerator.fromProgram(program, '/');
+ const doc = docGenerator.toDoc(apiInstance);
+
+ expect(doc.id).toBe('my-id');
+ expect(doc.description).toBe('my-description');
+ expect(doc.name).toBe('myApi');
+ expect(doc.file).toBe('mem/index.ts');
+ expect(doc.lineInFile).toBe(6);
+ expect(doc.interfaceInfos).toEqual([
+ {
+ dependentTypes: [],
+ docs: [],
+ file: 'mem/index.ts',
+ lineInFile: 4,
+ members: [],
+ name: 'MyApiType',
+ },
+ ]);
+ });
+
+ it('should generate API docs', () => {
+ const program = createMemProgram(
+ `
+ import MyApi from './type';
+
+ /** MySubSubType Docs */
+ type MySubSubType = { n: number; };
+
+ /** MySubType Docs */
+ type MySubType = {
+ /** Field a docs */
+ a: boolean;
+ // Field b docs
+ b: MySubSubType;
+ }
+
+ /** MySecondSubType Docs */
+
+ /** With multiple comments */
+ export type MySecondSubType = { s: string };
+
+ // MyThirdSubType Docs that shouldn't show up
+ type MyThirdSubType = { b: boolean };
+
+ /** MyApiType Docs */
+ type MyApiType = {
+ /** Docs for x */
+ x: string;
+ // Line comments shouldn't show up
+ y: MySubType;
+ /** Multiple */
+ /** JsDoc */
+ /** Comments */
+ z(a: Promise): Array;
+ };
+
+ /** Should not show up */
+ export const myApi = new MyApi({
+ id: 'my-id',
+ description: 'my-description',
+ });
+ `,
+ {
+ '/mem/type.ts': `export default class MyApi {
+ constructor(private readonly info: { id: string, description: string }) {}
+ }`,
+ },
+ );
+
+ const source = program.getSourceFile('/mem/index.ts');
+
+ // Figure out type IDs so we can make sure they match later
+ const checker = program.getTypeChecker();
+ const symbols = checker.getSymbolsInScope(
+ source!.getChildren().slice(-1)[0],
+ ts.SymbolFlags.TypeAlias,
+ );
+ const Ids = [
+ 'MySubType',
+ 'MySubSubType',
+ 'MySecondSubType',
+ 'MyThirdSubType',
+ ].reduce((ids, name) => {
+ const symbol = symbols.find(s => s.escapedName === name)!;
+ const type = checker.getTypeAtLocation(symbol.declarations[0]);
+ ids[name] = (type.aliasSymbol as any).id;
+ return ids;
+ }, {} as { [key in string]: number });
+
+ const typeLocator = TypeLocator.fromProgram(program, '/mem');
+
+ const { apiInstances } = typeLocator.findExportedInstances({
+ apiInstances: typeLocator.getExportedType('/mem/type.ts'),
+ });
+
+ expect(apiInstances.length).toBe(1);
+ const [apiInstance] = apiInstances;
+
+ const docGenerator = ApiDocGenerator.fromProgram(program, '/mem');
+ const doc = docGenerator.toDoc(apiInstance);
+
+ expect(doc.id).toBe('my-id');
+ expect(doc.description).toBe('my-description');
+ expect(doc.name).toBe('myApi');
+ expect(doc.file).toBe('index.ts');
+ expect(doc.interfaceInfos.length).toBe(1);
+ expect(doc.interfaceInfos[0].docs).toEqual(['MyApiType Docs']);
+ expect(doc.interfaceInfos[0].file).toBe('index.ts');
+ expect(doc.interfaceInfos[0].lineInFile).toBe(24);
+ expect(doc.interfaceInfos[0].name).toBe('MyApiType');
+ expect(doc.interfaceInfos[0].members).toEqual([
+ {
+ type: 'prop',
+ name: 'x',
+ path: 'MyApiType.x',
+ text: 'x: string',
+ docs: ['Docs for x'],
+ links: [],
+ },
+ {
+ type: 'prop',
+ name: 'y',
+ path: 'MyApiType.y',
+ text: 'y: MySubType',
+ docs: [],
+ links: [
+ {
+ id: Ids.MySubType,
+ name: 'MySubType',
+ path: 'index.ts/MySubType',
+ location: [3, 12],
+ },
+ ],
+ },
+ {
+ type: 'method',
+ name: 'z',
+ path: 'MyApiType.z',
+ text:
+ 'z(a: Promise): Array',
+ docs: ['Multiple', 'JsDoc', 'Comments'],
+ links: [
+ {
+ id: Ids.MySecondSubType,
+ name: 'MySecondSubType',
+ path: 'index.ts/MySecondSubType',
+ location: [27, 42],
+ },
+ {
+ id: Ids.MyThirdSubType,
+ name: 'MyThirdSubType',
+ path: 'index.ts/MyThirdSubType',
+ location: [56, 70],
+ },
+ ],
+ },
+ ]);
+ expect(doc.interfaceInfos[0].dependentTypes).toEqual([
+ {
+ id: Ids.MySubType,
+ name: 'MySubType',
+ path: 'index.ts/MySubType',
+ file: 'index.ts',
+ lineInFile: 8,
+ text: `type MySubType = {
+ /** Field a docs */
+ a: boolean;
+ // Field b docs
+ b: MySubSubType;
+ }`,
+ docs: ['MySubType Docs'],
+ links: [
+ {
+ id: Ids.MySubSubType,
+ name: 'MySubSubType',
+ path: 'index.ts/MySubSubType',
+ location: [102, 114],
+ },
+ ],
+ children: [],
+ },
+ {
+ id: Ids.MySubSubType,
+ name: 'MySubSubType',
+ path: 'index.ts/MySubSubType',
+ file: 'index.ts',
+ lineInFile: 5,
+ text: 'type MySubSubType = { n: number; }',
+ docs: ['MySubSubType Docs'],
+ links: [],
+ children: [],
+ },
+ {
+ id: Ids.MySecondSubType,
+ name: 'MySecondSubType',
+ path: 'index.ts/MySecondSubType',
+ file: 'index.ts',
+ lineInFile: 18,
+ text: 'export type MySecondSubType = { s: string }',
+ docs: ['MySecondSubType Docs', 'With multiple comments'],
+ links: [],
+ children: [],
+ },
+ {
+ id: Ids.MyThirdSubType,
+ name: 'MyThirdSubType',
+ path: 'index.ts/MyThirdSubType',
+ file: 'index.ts',
+ lineInFile: 21,
+ text: 'type MyThirdSubType = { b: boolean }',
+ docs: [],
+ links: [],
+ children: [],
+ },
+ ]);
+ });
+});
diff --git a/packages/docgen/src/docgen/ApiDocGenerator.ts b/packages/docgen/src/docgen/ApiDocGenerator.ts
new file mode 100644
index 0000000000..bfe449155b
--- /dev/null
+++ b/packages/docgen/src/docgen/ApiDocGenerator.ts
@@ -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({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;
+ 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 = new Set(),
+ ): { links: TypeLink[]; infos: TypeInfo[] } => {
+ if (!node || visited.has(node)) {
+ return { links: [], infos: [] };
+ }
+ // This makes sure we don't end up in a loop.
+ // It doesn't exclude repeated types, e.g. Array>, since we're exiting on duplicate nodes, not types.
+ visited.add(node);
+
+ const children = node
+ .getChildren()
+ .flatMap(child => this.findAllTypeReferences(child, visited));
+
+ if (!ts.isTypeReferenceNode(node)) {
+ return {
+ links: children.flatMap(child => child.links),
+ infos: children.flatMap(child => child.infos),
+ };
+ }
+
+ const type = this.checker.getTypeFromTypeNode(node);
+
+ const info = this.validateTypeDeclaration(type);
+ if (!info) {
+ return {
+ links: children.flatMap(child => child.links),
+ infos: children.flatMap(child => child.infos),
+ };
+ }
+ const { symbol, declaration } = info;
+
+ const typeRefs = this.findAllTypeReferences(declaration, visited);
+
+ const sourceFile = declaration.getSourceFile();
+ const { line } = sourceFile.getLineAndCharacterOfPosition(
+ declaration.getStart(),
+ );
+ const file = 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();
+ return flatDescs.filter(desc => {
+ if (seenTypes.has(desc.id)) {
+ return false;
+ }
+ seenTypes.add(desc.id);
+ return true;
+ });
+ }
+
+ /**
+ * Calculate link positions within a block of code, with 0 being
+ * the first character in the block.
+ */
+ private recalculateLinkOffsets(
+ parent: ts.Node,
+ links: TypeLink[],
+ ): TypeLink[] {
+ const parentStart = parent.getStart();
+ return links.map(link => {
+ const [start, end] = link.location;
+ return {
+ ...link,
+ location: [start - parentStart, end - parentStart],
+ };
+ });
+ }
+}
diff --git a/packages/docgen/src/docgen/ApiDocsPrinter.ts b/packages/docgen/src/docgen/ApiDocsPrinter.ts
new file mode 100644
index 0000000000..fdd53afc8e
--- /dev/null
+++ b/packages/docgen/src/docgen/ApiDocsPrinter.ts
@@ -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$/, '');
+ }
+}
diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts
new file mode 100644
index 0000000000..64d38d8b15
--- /dev/null
+++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts
@@ -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('
');
+ }
+
+ private escapeText: (text: string) => string = (() => {
+ const escapes: { [char in string]: string } = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ };
+
+ return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
+ })();
+
+ private formatWithLinks({
+ text,
+ links = [],
+ }: {
+ text: string;
+ links?: TypeLink[];
+ }): string {
+ const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
+
+ sortedLinks.reduce((lastEnd, link) => {
+ if (link.location[0] <= lastEnd) {
+ throw new Error(
+ `overlapping link detected for ${link.path}, ${link.location}`,
+ );
+ }
+ return link.location[1];
+ }, -1);
+
+ const parts: Array<{ text: string; path?: string }> = [];
+
+ const endLocation = sortedLinks.reduce((prev, link) => {
+ const [start, end] = link.location;
+ parts.push(
+ { text: text.slice(prev, start) },
+ { text: text.slice(start, end), path: link.path },
+ );
+ return end;
+ }, 0);
+
+ parts.push({ text: text.slice(endLocation) });
+
+ return parts
+ .map(part => {
+ if (part.path) {
+ const link = this.headerLink(part.text, part.path);
+ return `${this.escapeText(part.text)}`;
+ }
+ return this.highlighter.highlight(this.escapeText(part.text));
+ })
+ .join('');
+ }
+
+ toBuffer(): Buffer {
+ return Buffer.from(this.str, 'utf8');
+ }
+}
diff --git a/packages/docgen/src/docgen/TypeLocator.test.ts b/packages/docgen/src/docgen/TypeLocator.test.ts
new file mode 100644
index 0000000000..5a264fd41c
--- /dev/null
+++ b/packages/docgen/src/docgen/TypeLocator.test.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ts from 'typescript';
+import TypeLocator from './TypeLocator';
+import { createMemProgram } from './testUtils';
+
+describe('TypeLocator', () => {
+ it('should find a default export', () => {
+ const program = createMemProgram('export default class MyApi {}');
+
+ const typeLocator = TypeLocator.fromProgram(program, '/mem');
+
+ const apiType = typeLocator.getExportedType('/mem/index.ts');
+ expect(apiType.symbol.name).toBe('default');
+ const [declaration] = apiType.symbol.declarations;
+ expect(declaration.kind).toBe(ts.SyntaxKind.ClassDeclaration);
+ expect((declaration as ts.ClassDeclaration).name).toBeDefined();
+ expect((declaration as ts.ClassDeclaration).name!.text).toBe('MyApi');
+ }, 10000);
+
+ it('should find api instance export', () => {
+ const program = createMemProgram(
+ `
+ import MyApi from './type';
+
+ type MyApiType = {};
+
+ export const myApi = new MyApi();
+ `,
+ {
+ '/mem/type.ts': 'export default class MyApi {}',
+ },
+ );
+
+ const typeLocator = TypeLocator.fromProgram(program, '/mem');
+
+ const { apiInstances } = typeLocator.findExportedInstances({
+ apiInstances: typeLocator.getExportedType('/mem/type.ts'),
+ });
+
+ expect(apiInstances.length).toBe(1);
+ const [apiInstance] = apiInstances;
+ expect(apiInstance.name).toBe('myApi');
+ expect(apiInstance.source.fileName).toBe('/mem/index.ts');
+ expect(apiInstance.args).toEqual([]);
+
+ expect(apiInstance.typeArgs.length).toBe(1);
+ const [typeArg] = apiInstance.typeArgs;
+ expect(typeArg.kind).toBe(ts.SyntaxKind.TypeReference);
+ expect((typeArg as ts.TypeReferenceNode).typeName.getText()).toBe(
+ 'MyApiType',
+ );
+ });
+});
diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts
new file mode 100644
index 0000000000..6030258112
--- /dev/null
+++ b/packages/docgen/src/docgen/TypeLocator.ts
@@ -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(
+ typeLookupTable: { [key in T]: ts.Type },
+ ): { [key in T]: ExportedInstance[] } {
+ const docMap = new Map();
+ for (const type of Object.values(typeLookupTable)) {
+ docMap.set(type, []);
+ }
+
+ this.program.getSourceFiles().forEach(source => {
+ const inRoot = source.fileName.startsWith(this.sourcePath);
+ if (!inRoot) {
+ return;
+ }
+ ts.forEachChild(source, node => {
+ const decl = this.getExportedConstructorDeclaration(node);
+ if (!decl || !docMap.has(decl.constructorType)) {
+ return;
+ }
+
+ docMap.get(decl.constructorType)!.push({
+ node,
+ 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 };
+ }
+}
diff --git a/packages/docgen/src/docgen/TypescriptHighlighter.ts b/packages/docgen/src/docgen/TypescriptHighlighter.ts
new file mode 100644
index 0000000000..ffdd4f1906
--- /dev/null
+++ b/packages/docgen/src/docgen/TypescriptHighlighter.ts
@@ -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: `${text}`,
+ highlighted: true,
+ };
+ }
+ return { text };
+ });
+ };
+
+ // Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted
+ return TypescriptHighlighter.highlighters
+ .reduce((parts, highlighter) => parts.flatMap(painter(...highlighter)), [
+ { text: fullText },
+ ])
+ .map(({ text }) => text)
+ .join('');
+ }
+}
diff --git a/packages/docgen/src/docgen/sortSelector.test.ts b/packages/docgen/src/docgen/sortSelector.test.ts
new file mode 100644
index 0000000000..a3da3e2950
--- /dev/null
+++ b/packages/docgen/src/docgen/sortSelector.test.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import sortSelector from './sortSelector';
+
+describe('sortSelector', () => {
+ it('should stable sort', () => {
+ const arr = [
+ [3, 1],
+ [1, 2],
+ [1, 1],
+ [1, 3],
+ [2, 1],
+ ];
+
+ const sortedByFirst = arr.slice().sort(sortSelector(([first]) => first));
+ expect(sortedByFirst).toEqual([
+ [1, 2],
+ [1, 1],
+ [1, 3],
+ [2, 1],
+ [3, 1],
+ ]);
+
+ const sortedBySecond = arr
+ .slice()
+ .sort(sortSelector(([, second]) => second));
+ expect(sortedBySecond).toEqual([
+ [3, 1],
+ [1, 1],
+ [2, 1],
+ [1, 2],
+ [1, 3],
+ ]);
+ });
+});
diff --git a/packages/docgen/src/docgen/sortSelector.ts b/packages/docgen/src/docgen/sortSelector.ts
new file mode 100644
index 0000000000..7590ae39ec
--- /dev/null
+++ b/packages/docgen/src/docgen/sortSelector.ts
@@ -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(
+ selector: (x: T) => any,
+): (a: T, b: T) => -1 | 1 | 0 {
+ return (a: T, b: T) => {
+ const aV = selector(a);
+ const bV = selector(b);
+ if (aV < bV) {
+ return -1;
+ } else if (aV > bV) {
+ return 1;
+ }
+ return 0;
+ };
+}
diff --git a/packages/docgen/src/docgen/testUtils.ts b/packages/docgen/src/docgen/testUtils.ts
new file mode 100644
index 0000000000..552776016a
--- /dev/null
+++ b/packages/docgen/src/docgen/testUtils.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ts from 'typescript';
+
+export function createMemProgram(
+ indexSource: string,
+ otherSourceFiles: { [fileName in string]: string } = {},
+): ts.Program {
+ const rootDir = '/mem';
+
+ const options = { noEmit: true };
+ const baseHost = ts.createCompilerHost(options);
+ const files: { [fileName in string]: string } = {
+ [`${rootDir}/index.ts`]: indexSource,
+ ...otherSourceFiles,
+ };
+
+ // Custom compiler hosts that reads from a map of in-memory files, but
+ // falls back to reading from disc for ts libs etc.
+ const compilerHost: ts.CompilerHost = {
+ ...baseHost,
+ readFile(fileName): string | undefined {
+ if (fileName in files) {
+ return files[fileName];
+ }
+ return baseHost.readFile(fileName);
+ },
+ getCurrentDirectory(): string {
+ return rootDir;
+ },
+ directoryExists(dir) {
+ if (dir === rootDir) {
+ return true;
+ }
+ if (baseHost.directoryExists) {
+ return baseHost.directoryExists(dir);
+ }
+ return false;
+ },
+ fileExists(fileName: string): boolean {
+ return !!files[fileName] || baseHost.fileExists(fileName);
+ },
+ getSourceFile(fileName, ...rest): ts.SourceFile | undefined {
+ const file = files[fileName];
+ if (file) {
+ return ts.createSourceFile(fileName, file, ts.ScriptTarget.ES2017);
+ }
+ return baseHost.getSourceFile(fileName, ...rest);
+ },
+ };
+
+ return ts.createProgram(['/mem/index.ts'], options, compilerHost);
+}
diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts
new file mode 100644
index 0000000000..7aff54d04c
--- /dev/null
+++ b/packages/docgen/src/docgen/types.ts
@@ -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;
+ dependentTypes: TypeInfo[];
+};
+
+/**
+ * ApiDoc describes a documented API.
+ */
+export type ApiDoc = {
+ id: string;
+ name: string;
+ description: string;
+ file: string;
+ lineInFile: number;
+ interfaceInfos: InterfaceInfo[];
+};
+
+/**
+ * ExportedInstance describes an expression matching `export {name} = new {Contructor}<{typeArgs}...>({args}...)`
+ */
+export type ExportedInstance = {
+ node: ts.Node;
+ name: string;
+ source: ts.SourceFile;
+ args: Array;
+ typeArgs: Array;
+};
+
+export type Highlighter = {
+ highlight(test: string): string;
+};
+
+/**
+ * Markdown printer is an abstraction for printing markdown documents of different flavors.
+ */
+export type MarkdownPrinter = {
+ text(text: string): void;
+ header(level: number, text: string, id?: string): void;
+ paragraph(...text: string[]): void;
+ code(options: { text: string; links?: TypeLink[] }): void;
+
+ headerLink(header: string, id?: string): string;
+ /** Link from pages to index */
+ indexLink(): string;
+ /** Link from index to pages */
+ pageLink(name: string): string;
+ srcLink(
+ { file, lineInFile }: { file: string; lineInFile: number },
+ text?: string,
+ ): string;
+
+ toBuffer(): Buffer;
+};
diff --git a/packages/docgen/src/generate.ts b/packages/docgen/src/generate.ts
new file mode 100644
index 0000000000..3587e2776b
--- /dev/null
+++ b/packages/docgen/src/generate.ts
@@ -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);
+ }
+ }
+}
diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts
new file mode 100644
index 0000000000..866299151a
--- /dev/null
+++ b/packages/docgen/src/index.ts
@@ -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