+
{subheader &&
{subheader}
}
{icon}
diff --git a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx
index 1d336fc5f5..f7df3d43cc 100644
--- a/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx
+++ b/packages/core-components/src/layout/ItemCard/ItemCard.stories.tsx
@@ -21,7 +21,7 @@ import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import React from 'react';
-import { MemoryRouter } from 'react-router';
+import { MemoryRouter } from 'react-router-dom';
import { Button } from '../../components';
import { ItemCardGrid } from './ItemCardGrid';
import { ItemCardHeader } from './ItemCardHeader';
diff --git a/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx b/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx
index 885b43ca34..a1e6d24aeb 100644
--- a/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx
+++ b/packages/core-components/src/layout/ItemCard/ItemCardGrid.tsx
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import Box from '@material-ui/core/Box';
import {
createStyles,
makeStyles,
@@ -65,8 +65,8 @@ export function ItemCardGrid(props: ItemCardGridProps) {
const { children, ...otherProps } = props;
const classes = useStyles(otherProps);
return (
-
+
{children}
-
+
);
}
diff --git a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx
index 76cf668079..da3d9b7ebe 100644
--- a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx
+++ b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { BackstageTheme } from '@backstage/theme';
+import Box from '@material-ui/core/Box';
import { createStyles, makeStyles, WithStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React from 'react';
-import { BackstageTheme } from '@backstage/theme';
/** @public */
export type ItemCardHeaderClassKey = 'root';
@@ -77,7 +77,7 @@ export function ItemCardHeader(props: ItemCardHeaderProps) {
const { title, subtitle, children } = props;
const classes = useStyles(props);
return (
-
+
{subtitle && (
{subtitle}
@@ -89,6 +89,6 @@ export function ItemCardHeader(props: ItemCardHeaderProps) {
)}
{children}
-
+
);
}
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
index a3f6c185f8..b8c2b84085 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
@@ -48,7 +48,7 @@ describe('ProxiedSignInIdentity', () => {
it('handles a token that has no exp', async () => {
const [a, _b, c] = validBackstageToken.split('.');
- const botched = `${a}.${btoa(JSON.stringify({}))}.${c}`;
+ const botched = `${a}.${window.btoa(JSON.stringify({}))}.${c}`;
expect(tokenToExpiry(botched)).toEqual(
new Date(new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis)),
);
@@ -81,16 +81,18 @@ describe('ProxiedSignInIdentity', () => {
backstageIdentity: {
token: [
'eyJhbGciOiJFUzI1NiIsImtpZCI6ImMxNTMzNDRiLWZjYzktNGIwOS1iN2ZhLTU3ZmM5MDhjMjBiNiJ9',
- btoa(
- JSON.stringify({
- iss: 'http://localhost:7007/api/auth',
- sub: 'user:default/freben',
- aud: 'backstage',
- iat,
- exp,
- ent: ['group:default/my-team'],
- }),
- ).replace(/=/g, ''),
+ window
+ .btoa(
+ JSON.stringify({
+ iss: 'http://localhost:7007/api/auth',
+ sub: 'user:default/freben',
+ aud: 'backstage',
+ iat,
+ exp,
+ ent: ['group:default/my-team'],
+ }),
+ )
+ .replace(/=/g, ''),
'4nOTmPHPwhzaKTzikgUsHcszfcP-JamcojMnRfyfsKhyHCCEywe6uLFlvvmK5NbaX5Z7IIji-kg7bxKU58kwoQ',
].join('.'),
identity: {
@@ -101,7 +103,6 @@ describe('ProxiedSignInIdentity', () => {
},
};
}
-
worker.events.on('request:match', serverCalled);
worker.use(
rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
@@ -162,5 +163,137 @@ describe('ProxiedSignInIdentity', () => {
await identity.getSessionAsync(); // now the expiry has passed
expect(serverCalled).toHaveBeenCalledTimes(2);
});
+
+ // dummy response for tests which are only testing the request behaviour
+ const dummySessionResponse = {
+ providerInfo: {},
+ profile: {},
+ backstageIdentity: {
+ token: '',
+ identity: {
+ ownershipEntityRefs: [''],
+ userEntityRef: '',
+ type: 'user',
+ },
+ },
+ };
+
+ it('handles headers passed as a promise', async () => {
+ let req1: Request;
+ const getBaseUrl = jest.fn();
+ const serverCalled = jest.fn().mockImplementation(req => {
+ req1 = req;
+ });
+
+ worker.events.on('request:match', serverCalled);
+ worker.use(
+ rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(dummySessionResponse),
+ ),
+ ),
+ );
+
+ const getHeaders = jest.fn().mockResolvedValue({ 'x-foo': 'bars' });
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ headers: getHeaders,
+ });
+
+ getBaseUrl.mockResolvedValue('http://example.com/api/auth');
+
+ await identity.start(); // should not throw
+ expect(getBaseUrl).toHaveBeenCalledTimes(1);
+ expect(getBaseUrl).toHaveBeenLastCalledWith('auth');
+ expect(getHeaders).toHaveBeenCalledTimes(1);
+ expect(serverCalled).toHaveBeenCalledTimes(1);
+
+ expect(req1!).not.toBeUndefined();
+ // required header should be present
+ expect(req1!.headers.get('x-requested-with')).toEqual('XMLHttpRequest');
+ // optional header should be present when passed
+ expect(req1!.headers.get('x-foo')).toEqual('bars');
+ });
+
+ it('handles headers passed as an object', async () => {
+ let req1: Request;
+ const getBaseUrl = jest.fn();
+ const serverCalled = jest.fn().mockImplementation(req => {
+ req1 = req;
+ });
+
+ worker.events.on('request:match', serverCalled);
+ worker.use(
+ rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(dummySessionResponse),
+ ),
+ ),
+ );
+
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ headers: { 'x-foo': 'bars' },
+ });
+
+ getBaseUrl.mockResolvedValue('http://example.com/api/auth');
+
+ await identity.start(); // should not throw
+ expect(getBaseUrl).toHaveBeenCalledTimes(1);
+ expect(getBaseUrl).toHaveBeenLastCalledWith('auth');
+ expect(serverCalled).toHaveBeenCalledTimes(1);
+
+ expect(req1!).not.toBeUndefined();
+ // required header should be present
+ expect(req1!.headers.get('x-requested-with')).toEqual('XMLHttpRequest');
+ // optional header should be present when passed
+ expect(req1!.headers.get('x-foo')).toEqual('bars');
+ });
+
+ it('handles headers passed as a function', async () => {
+ let req1: Request;
+ const getBaseUrl = jest.fn();
+ const serverCalled = jest.fn().mockImplementation(req => {
+ req1 = req;
+ });
+
+ worker.events.on('request:match', serverCalled);
+ worker.use(
+ rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(dummySessionResponse),
+ ),
+ ),
+ );
+
+ const getHeaders = jest.fn().mockReturnValue({ 'x-foo': 'bars' });
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ headers: getHeaders,
+ });
+
+ getBaseUrl.mockResolvedValue('http://example.com/api/auth');
+
+ await identity.start(); // should not throw
+ expect(getBaseUrl).toHaveBeenCalledTimes(1);
+ expect(getBaseUrl).toHaveBeenLastCalledWith('auth');
+ expect(getHeaders).toHaveBeenCalledTimes(1);
+ expect(serverCalled).toHaveBeenCalledTimes(1);
+
+ expect(req1!).not.toBeUndefined();
+ // required header should be present
+ expect(req1!.headers.get('x-requested-with')).toEqual('XMLHttpRequest');
+ // optional header should be present when passed
+ expect(req1!.headers.get('x-foo')).toEqual('bars');
+ });
});
});
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
index 2247d17d82..bd1fc1bf39 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
@@ -40,7 +40,7 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
}
const [_header, rawPayload, _signature] = jwtToken.split('.');
- const payload = JSON.parse(atob(rawPayload));
+ const payload = JSON.parse(window.atob(rawPayload));
if (typeof payload.exp !== 'number') {
return fallback;
}
@@ -51,6 +51,7 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
+ headers?: HeadersInit | (() => HeadersInit) | (() => Promise
);
};
type State =
@@ -193,6 +194,13 @@ export class ProxiedSignInIdentity implements IdentityApi {
async fetchSession(): Promise {
const baseUrl = await this.options.discoveryApi.getBaseUrl('auth');
+ const headers =
+ typeof this.options.headers === 'function'
+ ? await this.options.headers()
+ : this.options.headers;
+ const mergedHeaders = new Headers(headers);
+ mergedHeaders.set('X-Requested-With', 'XMLHttpRequest');
+
// Note that we do not use the fetchApi here, since this all happens before
// sign-in completes so there can be no automatic token injection and
// similar.
@@ -200,7 +208,7 @@ export class ProxiedSignInIdentity implements IdentityApi {
`${baseUrl}/${this.options.provider}/refresh`,
{
signal: this.abortController.signal,
- headers: { 'x-requested-with': 'XMLHttpRequest' },
+ headers: mergedHeaders,
credentials: 'include',
},
);
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
index 965c603004..ea095cd8ab 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
@@ -36,6 +36,12 @@ export type ProxiedSignInPageProps = SignInPageProps & {
* a properly configured auth provider ID in the auth backend.
*/
provider: string;
+
+ /**
+ * Optional headers which are passed along with the request to the
+ * underlying provider
+ */
+ headers?: HeadersInit | (() => HeadersInit) | (() => Promise);
};
/**
@@ -60,6 +66,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const identity = new ProxiedSignInIdentity({
provider: props.provider,
discoveryApi,
+ headers: props.headers,
});
await identity.start();
diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx
index c86f60a0a2..95ed88b1ec 100644
--- a/packages/core-components/src/layout/Sidebar/Bar.tsx
+++ b/packages/core-components/src/layout/Sidebar/Bar.tsx
@@ -13,28 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { BackstageTheme } from '@backstage/theme';
+import Box from '@material-ui/core/Box';
+import Button from '@material-ui/core/Button';
import { makeStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import classnames from 'classnames';
-
-import React, { useState, useContext, useRef } from 'react';
-import Button from '@material-ui/core/Button';
+import React, { useContext, useRef, useState } from 'react';
import {
makeSidebarConfig,
makeSidebarSubmenuConfig,
SidebarConfig,
SidebarConfigContext,
- SubmenuConfig,
SidebarOptions,
+ SubmenuConfig,
SubmenuOptions,
} from './config';
-import { BackstageTheme } from '@backstage/theme';
+import { MobileSidebar } from './MobileSidebar';
import { useContent } from './Page';
import { SidebarOpenStateProvider } from './SidebarOpenStateContext';
import { useSidebarPinState } from './SidebarPinStateContext';
-import { MobileSidebar } from './MobileSidebar';
/** @public */
export type SidebarClassKey = 'drawer' | 'drawerOpen';
@@ -64,6 +63,9 @@ const useStyles = makeStyles(
'&::-webkit-scrollbar': {
display: 'none',
},
+ '& .MuiButtonBase-root': {
+ textTransform: 'none',
+ },
}),
drawerOpen: props => ({
width: props.sidebarConfig.drawerWidthOpen,
@@ -191,7 +193,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
);
diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx
index 91e188b203..824e9e42fd 100644
--- a/packages/core-components/src/layout/Sidebar/Intro.tsx
+++ b/packages/core-components/src/layout/Sidebar/Intro.tsx
@@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import { BackstageTheme } from '@backstage/theme';
+import Box from '@material-ui/core/Box';
import Collapse from '@material-ui/core/Collapse';
-import { makeStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
+import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
-import React, { useContext, useState } from 'react';
import { useLocalStorageValue } from '@react-hookz/web';
+import React, { useContext, useState } from 'react';
+
import {
- SidebarConfigContext,
- SidebarConfig,
SIDEBAR_INTRO_LOCAL_STORAGE,
+ SidebarConfig,
+ SidebarConfigContext,
} from './config';
import { SidebarDivider } from './Items';
import { useSidebarOpenState } from './SidebarOpenStateContext';
@@ -45,8 +46,8 @@ const useStyles = makeStyles(
// XXX (@koroeskohr): should I be using a Mui theme variable?
fontSize: 12,
width: props.sidebarConfig.drawerWidthOpen,
- marginTop: 18,
- marginBottom: 12,
+ marginTop: theme.spacing(2.25),
+ marginBottom: theme.spacing(1.5),
paddingLeft: props.sidebarConfig.iconPadding,
paddingRight: props.sidebarConfig.iconPadding,
}),
@@ -54,13 +55,13 @@ const useStyles = makeStyles(
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
- marginTop: 12,
+ marginTop: theme.spacing(1.5),
},
introDismissLink: {
color: '#dddddd',
display: 'flex',
alignItems: 'center',
- marginBottom: 4,
+ marginBottom: theme.spacing(0.5),
'&:hover': {
color: theme.palette.linkHover,
transition: theme.transitions.create('color', {
@@ -78,7 +79,7 @@ const useStyles = makeStyles(
introDismissIcon: {
width: 18,
height: 18,
- marginRight: 12,
+ marginRight: theme.spacing(1.5),
},
}),
{ name: 'BackstageSidebarIntro' },
@@ -103,17 +104,17 @@ export function IntroCard(props: IntroCardProps) {
const handleClose = () => onClose();
return (
-
+
+
);
}
@@ -151,7 +152,7 @@ export function SidebarIntro(_props: {}) {
starredItemsDismissed: false,
recentlyViewedItemsDismissed: false,
};
- const [dismissedIntro, setDismissedIntro] =
+ const { value: dismissedIntro, set: setDismissedIntro } =
useLocalStorageValue(SIDEBAR_INTRO_LOCAL_STORAGE);
const { starredItemsDismissed, recentlyViewedItemsDismissed } =
diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx
index 0f81f7ce63..eafbf3827e 100644
--- a/packages/core-components/src/layout/Sidebar/Items.test.tsx
+++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx
@@ -23,7 +23,7 @@ import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import { Sidebar } from './Bar';
import { SidebarItem, SidebarSearchField, SidebarExpandButton } from './Items';
import { renderHook } from '@testing-library/react-hooks';
-import { hexToRgb, makeStyles } from '@material-ui/core/styles';
+import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
spotlight: {
@@ -71,7 +71,7 @@ describe('Items', () => {
it('should render a button with custom style', async () => {
expect(
await screen.findByRole('button', { name: /create/i }),
- ).toHaveStyle(`background-color: ${hexToRgb('2b2a2a')}`);
+ ).toHaveStyle(`background-color: transparent`);
});
});
describe('SidebarSearchField', () => {
diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx
index fe8dede499..823320403e 100644
--- a/packages/core-components/src/layout/Sidebar/Items.tsx
+++ b/packages/core-components/src/layout/Sidebar/Items.tsx
@@ -13,23 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import { IconComponent, useElementFilter } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
-import { makeStyles, styled, Theme } from '@material-ui/core/styles';
-import useMediaQuery from '@material-ui/core/useMediaQuery';
import Badge from '@material-ui/core/Badge';
-import TextField from '@material-ui/core/TextField';
-import Typography from '@material-ui/core/Typography';
+import Box from '@material-ui/core/Box';
+import { makeStyles, styled, Theme } from '@material-ui/core/styles';
import {
CreateCSSProperties,
StyledComponentProps,
} from '@material-ui/core/styles/withStyles';
+import TextField from '@material-ui/core/TextField';
+import Typography from '@material-ui/core/Typography';
+import useMediaQuery from '@material-ui/core/useMediaQuery';
+import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
+import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import SearchIcon from '@material-ui/icons/Search';
-import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
-import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import classnames from 'classnames';
+import { Location } from 'history';
import React, {
ComponentProps,
ComponentType,
@@ -48,18 +49,19 @@ import {
useLocation,
useResolvedPath,
} from 'react-router-dom';
+
import {
+ SidebarConfig,
SidebarConfigContext,
SidebarItemWithSubmenuContext,
- SidebarConfig,
} from './config';
-import { SidebarSubmenuProps, SidebarSubmenu } from './SidebarSubmenu';
import DoubleArrowLeft from './icons/DoubleArrowLeft';
import DoubleArrowRight from './icons/DoubleArrowRight';
-import { isLocationMatch } from './utils';
-import { Location } from 'history';
import { useSidebarOpenState } from './SidebarOpenStateContext';
+import { SidebarSubmenu, SidebarSubmenuProps } from './SidebarSubmenu';
import { SidebarSubmenuItemProps } from './SidebarSubmenuItem';
+import { isLocationMatch } from './utils';
+import Button from '@material-ui/core/Button';
/** @public */
export type SidebarItemClassKey =
@@ -139,13 +141,14 @@ const makeSidebarStyles = (sidebarConfig: SidebarConfig) =>
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
+ lineHeight: '0',
},
searchRoot: {
marginBottom: 12,
},
searchField: {
color: '#b5b5b5',
- fontWeight: 'bold',
+ fontWeight: theme.typography.fontWeightBold,
fontSize: theme.typography.fontSize,
},
searchFieldHTMLInput: {
@@ -370,13 +373,15 @@ const SidebarItemBase = forwardRef((props, ref) => {
const { isOpen } = useSidebarOpenState();
const divStyle =
- !isOpen && hasSubmenu ? { display: 'flex', marginLeft: '24px' } : {};
+ !isOpen && hasSubmenu
+ ? { display: 'flex', marginLeft: '20px' }
+ : { lineHeight: '0' };
const displayItemIcon = (
-
+
- {!isOpen && hasSubmenu ? : <>>}
-
+ {!isOpen && hasSubmenu ? : <>>}
+
);
const itemIcon = (
@@ -393,9 +398,9 @@ const SidebarItemBase = forwardRef((props, ref) => {
const openContent = (
<>
-
+
{itemIcon}
-
+
{text && (
{text}
@@ -420,9 +425,9 @@ const SidebarItemBase = forwardRef((props, ref) => {
if (isButtonItem(props)) {
return (
-
);
}
@@ -572,7 +577,7 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
};
return (
-
+
-
+
);
}
@@ -620,13 +625,13 @@ export const SidebarSpacer = styled('div')(
export type SidebarDividerClassKey = 'root';
export const SidebarDivider = styled('hr')(
- {
+ ({ theme }) => ({
height: 1,
width: '100%',
background: '#383838',
border: 'none',
- margin: '12px 0px',
- },
+ margin: theme.spacing(1.2, 0),
+ }),
{ name: 'BackstageSidebarDivider' },
) as ComponentType & StyledComponentProps<'root'>>;
@@ -687,15 +692,16 @@ export const SidebarExpandButton = () => {
};
return (
-
-
+
{isOpen ? : }
-
-
+
+
);
};
diff --git a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx
index bfa841690d..9000c2f10b 100644
--- a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx
+++ b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx
@@ -26,7 +26,7 @@ import CloseIcon from '@material-ui/icons/Close';
import MenuIcon from '@material-ui/icons/Menu';
import { orderBy } from 'lodash';
import React, { createContext, useEffect, useState, useContext } from 'react';
-import { useLocation } from 'react-router';
+import { useLocation } from 'react-router-dom';
import { SidebarOpenStateProvider } from './SidebarOpenStateContext';
import { SidebarGroup } from './SidebarGroup';
import { SidebarConfigContext, SidebarConfig } from './config';
diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx
index bdf41b5dd7..5f660547d1 100644
--- a/packages/core-components/src/layout/Sidebar/Page.tsx
+++ b/packages/core-components/src/layout/Sidebar/Page.tsx
@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { BackstageTheme } from '@backstage/theme';
+import Box from '@material-ui/core/Box';
import { makeStyles } from '@material-ui/core/styles';
-
+import useMediaQuery from '@material-ui/core/useMediaQuery';
import React, {
createContext,
useCallback,
@@ -25,10 +26,9 @@ import React, {
useRef,
useState,
} from 'react';
-import { SidebarConfigContext, SidebarConfig } from './config';
-import { BackstageTheme } from '@backstage/theme';
+
+import { SidebarConfig, SidebarConfigContext } from './config';
import { LocalStorage } from './localStorage';
-import useMediaQuery from '@material-ui/core/useMediaQuery';
import { SidebarPinStateProvider } from './SidebarPinStateContext';
export type SidebarPageClassKey = 'root';
@@ -122,7 +122,7 @@ export function SidebarPage(props: SidebarPageProps) {
}}
>
- {props.children}
+ {props.children}
);
diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx
index 438e580ef4..cc61a6a063 100644
--- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx
+++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx
@@ -13,17 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { BackstageTheme } from '@backstage/theme';
+import Box from '@material-ui/core/Box';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import classnames from 'classnames';
import React, { ReactNode, useContext, useEffect, useState } from 'react';
+
import {
- SidebarItemWithSubmenuContext,
SidebarConfigContext,
+ SidebarItemWithSubmenuContext,
SubmenuConfig,
} from './config';
import { useSidebarOpenState } from './SidebarOpenStateContext';
-import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles<
BackstageTheme,
@@ -76,10 +78,10 @@ const useStyles = makeStyles<
},
}),
title: {
- fontSize: 24,
- fontWeight: 500,
+ fontSize: theme.typography.h5.fontSize,
+ fontWeight: theme.typography.fontWeightMedium,
color: '#FFF',
- padding: 20,
+ padding: theme.spacing(2.5),
[theme.breakpoints.down('xs')]: {
display: 'none',
},
@@ -120,7 +122,7 @@ export const SidebarSubmenu = (props: SidebarSubmenuProps) => {
}, [isHoveredOn]);
return (
- {
{props.title}
{props.children}
-
+
);
};
diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx
index 51092d7e81..5fdd29f203 100644
--- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx
+++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx
@@ -26,6 +26,8 @@ import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
import { SidebarItemWithSubmenuContext } from './config';
import { isLocationMatch } from './utils';
+import Box from '@material-ui/core/Box';
+import Button from '@material-ui/core/Button';
const useStyles = makeStyles(
theme => ({
@@ -39,7 +41,7 @@ const useStyles = makeStyles(
display: 'flex',
alignItems: 'center',
color: theme.palette.navigation.color,
- padding: 20,
+ padding: theme.spacing(2.5),
cursor: 'pointer',
position: 'relative',
background: 'none',
@@ -53,9 +55,9 @@ const useStyles = makeStyles(
color: '#FFF',
},
label: {
- margin: 14,
- marginLeft: 7,
- fontSize: 14,
+ margin: theme.spacing(1.75),
+ marginLeft: theme.spacing(1),
+ fontSize: theme.typography.body2.fontSize,
whiteSpace: 'nowrap',
overflow: 'hidden',
'text-overflow': 'ellipsis',
@@ -88,7 +90,7 @@ const useStyles = makeStyles(
color: theme.palette.navigation.color,
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(1),
- fontSize: '14px',
+ fontSize: theme.typography.body2.fontSize,
whiteSpace: 'nowrap',
overflow: 'hidden',
'text-overflow': 'ellipsis',
@@ -155,9 +157,10 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
return isActive;
});
return (
-
+
- e.stopPropagation()}
className={classnames(
@@ -180,10 +183,10 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
) : (
)}
-
+
{dropdownItems && showDropDown && (
-
+
{dropdownItems.map((object, key) => (
{
))}
-
+
)}
-
+
);
}
return (
-
+
{
-
+
);
};
diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx
index eb487940a8..ba1640a860 100644
--- a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx
+++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import React from 'react';
+import Box from '@material-ui/core/Box';
import { makeStyles } from '@material-ui/core/styles';
import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
+import React from 'react';
const useStyles = makeStyles({
iconContainer: {
@@ -34,14 +34,14 @@ const DoubleArrowLeft = () => {
const classes = useStyles();
return (
-
+
+
);
};
diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx
index e1e9b9476d..3824ec263d 100644
--- a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx
+++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import React from 'react';
+import Box from '@material-ui/core/Box';
import { makeStyles } from '@material-ui/core/styles';
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
+import React from 'react';
const useStyles = makeStyles({
iconContainer: {
@@ -34,14 +34,14 @@ const DoubleArrowRight = () => {
const classes = useStyles();
return (
-
+
+
);
};
diff --git a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts
index cc51d2b9a8..3c1e4de7d8 100644
--- a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts
+++ b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts
@@ -22,7 +22,7 @@ import {
function parseJwtPayload(token: string) {
const [_header, payload, _signature] = token.split('.');
- return JSON.parse(atob(payload));
+ return JSON.parse(window.atob(payload));
}
type LegacySignInResult = {
diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx
index 77fff64fb5..6703fe25ce 100644
--- a/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx
+++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.stories.tsx
@@ -16,7 +16,7 @@
import Grid from '@material-ui/core/Grid';
import React, { PropsWithChildren, useState } from 'react';
-import { MemoryRouter } from 'react-router';
+import { MemoryRouter } from 'react-router-dom';
import { CardTab, TabbedCard } from './TabbedCard';
const cardContentStyle = { height: 200, width: 500 };
diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx
index b3b977f04b..90cc928169 100644
--- a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx
+++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx
@@ -96,7 +96,7 @@ export function TabbedCard(props: PropsWithChildren) {
} else {
React.Children.map(children, child => {
if (React.isValidElement(child) && child?.props.value === value) {
- selectedTabContent = child?.props.children;
+ selectedTabContent = child?.props?.children;
}
});
}
@@ -142,7 +142,7 @@ const useCardTabStyles = makeStyles(
},
},
selected: {
- fontWeight: 'bold',
+ fontWeight: theme.typography.fontWeightBold,
},
}),
{ name: 'BackstageCardTab' },
diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md
index 2704cbcb6d..4f8ff07e31 100644
--- a/packages/core-plugin-api/CHANGELOG.md
+++ b/packages/core-plugin-api/CHANGELOG.md
@@ -1,5 +1,75 @@
# @backstage/core-plugin-api
+## 1.2.0
+
+### Minor Changes
+
+- 9a1864976a: Added a new `display` property to the `AlertMessage` which can accept the values `permanent` or `transient`.
+
+ Here's a rough example of how to trigger an alert using the new `display` property:
+
+ ```ts
+ import { alertApiRef, useApi } from '@backstage/core-plugin-api';
+
+ const ExampleTransient = () => {
+ const alertApi = useApi(alertApiRef);
+ alertApi.post({
+ message: 'Example of Transient Alert',
+ severity: 'success',
+ display: 'transient',
+ });
+ };
+ ```
+
+### Patch Changes
+
+- d56127c712: useRouteRef - Limit re-resolving to location pathname changes only
+- 3280711113: Updated dependency `msw` to `^0.49.0`.
+- 19356df560: Updated dependency `zen-observable` to `^0.9.0`.
+- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`.
+- Updated dependencies
+ - @backstage/version-bridge@1.0.3
+ - @backstage/types@1.0.2
+ - @backstage/config@1.0.5
+
+## 1.2.0-next.2
+
+### Minor Changes
+
+- 9a1864976a: Added a new `display` property to the `AlertMessage` which can accept the values `permanent` or `transient`.
+
+ Here's a rough example of how to trigger an alert using the new `display` property:
+
+ ```ts
+ import { alertApiRef, useApi } from '@backstage/core-plugin-api';
+
+ const ExampleTransient = () => {
+ const alertApi = useApi(alertApiRef);
+ alertApi.post({
+ message: 'Example of Transient Alert',
+ severity: 'success',
+ display: 'transient',
+ });
+ };
+ ```
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.5-next.1
+ - @backstage/types@1.0.2-next.1
+ - @backstage/version-bridge@1.0.3-next.0
+
+## 1.1.1-next.1
+
+### Patch Changes
+
+- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`.
+- Updated dependencies
+ - @backstage/version-bridge@1.0.3-next.0
+ - @backstage/types@1.0.2-next.1
+ - @backstage/config@1.0.5-next.1
+
## 1.1.1-next.0
### Patch Changes
diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md
index cebf09e908..dff4f7e5a6 100644
--- a/packages/core-plugin-api/api-report.md
+++ b/packages/core-plugin-api/api-report.md
@@ -29,6 +29,7 @@ export const alertApiRef: ApiRef;
export type AlertMessage = {
message: string;
severity?: 'success' | 'info' | 'warning' | 'error';
+ display?: 'permanent' | 'transient';
};
// @public
diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json
index 56ec92b331..f33d194214 100644
--- a/packages/core-plugin-api/package.json
+++ b/packages/core-plugin-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-plugin-api",
"description": "Core API used by Backstage plugins",
- "version": "1.1.1-next.0",
+ "version": "1.2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -38,7 +38,7 @@
"@backstage/version-bridge": "workspace:^",
"history": "^5.0.0",
"prop-types": "^15.7.2",
- "zen-observable": "^0.9.0"
+ "zen-observable": "^0.10.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
diff --git a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts
index 0c84f7c975..afdcb6a617 100644
--- a/packages/core-plugin-api/src/apis/definitions/AlertApi.ts
+++ b/packages/core-plugin-api/src/apis/definitions/AlertApi.ts
@@ -26,6 +26,7 @@ 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';
+ display?: 'permanent' | 'transient';
};
/**
diff --git a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx
index 62f6d6da42..618f40d281 100644
--- a/packages/core-plugin-api/src/routing/useRouteRef.test.tsx
+++ b/packages/core-plugin-api/src/routing/useRouteRef.test.tsx
@@ -16,10 +16,11 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
-import { MemoryRouter } from 'react-router-dom';
+import { MemoryRouter, Router } from 'react-router-dom';
import { createVersionedContextForTesting } from '@backstage/version-bridge';
import { useRouteRef } from './useRouteRef';
import { createRouteRef } from './RouteRef';
+import { createBrowserHistory } from 'history';
describe('v1 consumer', () => {
const context = createVersionedContextForTesting('routing-context');
@@ -49,4 +50,108 @@ describe('v1 consumer', () => {
}),
);
});
+
+ it('re-resolves the routeFunc when the search parameters change', () => {
+ const resolve = jest.fn(() => () => '/hello');
+ context.set({ 1: { resolve } });
+
+ const routeRef = createRouteRef({ id: 'ref1' });
+ const history = createBrowserHistory();
+ history.push('/my-page');
+
+ const { rerender } = renderHook(() => useRouteRef(routeRef), {
+ wrapper: ({ children }) => (
+
+ ),
+ });
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+
+ history.push('/my-new-page');
+ rerender();
+
+ expect(resolve).toHaveBeenCalledTimes(2);
+ });
+
+ it('does not re-resolve the routeFunc the location pathname does not change', () => {
+ const resolve = jest.fn(() => () => '/hello');
+ context.set({ 1: { resolve } });
+
+ const routeRef = createRouteRef({ id: 'ref1' });
+ const history = createBrowserHistory();
+ history.push('/my-page');
+
+ const { rerender } = renderHook(() => useRouteRef(routeRef), {
+ wrapper: ({ children }) => (
+
+ ),
+ });
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+
+ history.push('/my-page');
+ rerender();
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not re-resolve the routeFunc when the search parameter changes', () => {
+ const resolve = jest.fn(() => () => '/hello');
+ context.set({ 1: { resolve } });
+
+ const routeRef = createRouteRef({ id: 'ref1' });
+ const history = createBrowserHistory();
+ history.push('/my-page');
+
+ const { rerender } = renderHook(() => useRouteRef(routeRef), {
+ wrapper: ({ children }) => (
+
+ ),
+ });
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+
+ history.push('/my-page?foo=bar');
+ rerender();
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not re-resolve the routeFunc when the hash parameter changes', () => {
+ const resolve = jest.fn(() => () => '/hello');
+ context.set({ 1: { resolve } });
+
+ const routeRef = createRouteRef({ id: 'ref1' });
+ const history = createBrowserHistory();
+ history.push('/my-page');
+
+ const { rerender } = renderHook(() => useRouteRef(routeRef), {
+ wrapper: ({ children }) => (
+
+ ),
+ });
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+
+ history.push('/my-page#foo');
+ rerender();
+
+ expect(resolve).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/core-plugin-api/src/routing/useRouteRef.tsx b/packages/core-plugin-api/src/routing/useRouteRef.tsx
index 563bb2035c..cf292b110f 100644
--- a/packages/core-plugin-api/src/routing/useRouteRef.tsx
+++ b/packages/core-plugin-api/src/routing/useRouteRef.tsx
@@ -85,7 +85,7 @@ export function useRouteRef(
| SubRouteRef
| ExternalRouteRef,
): RouteFunc | undefined {
- const sourceLocation = useLocation();
+ const { pathname } = useLocation();
const versionedContext = useVersionedContext<{ 1: RouteResolver }>(
'routing-context',
);
@@ -95,8 +95,8 @@ export function useRouteRef(
const resolver = versionedContext.atVersion(1);
const routeFunc = useMemo(
- () => resolver && resolver.resolve(routeRef, sourceLocation),
- [resolver, routeRef, sourceLocation],
+ () => resolver && resolver.resolve(routeRef, { pathname }),
+ [resolver, routeRef, pathname],
);
if (!versionedContext) {
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index e3885cc0eb..f9a5ff6219 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,151 @@
# @backstage/create-app
+## 0.4.35
+
+### Patch Changes
+
+- c4788dbb58: Fix dependency ordering in templated packages.
+- 83d3167594: Bumped create-app version.
+- 2cb6963f9b: Bumped create-app version.
+- 6465ab3686: Bumped create-app version.
+- af1358bb07: added default project name for CI job compatibility
+- 935b66a646: Change step output template examples to use square bracket syntax.
+- dfb269fab2: Updated the template to have the `'/test'` proxy endpoint in `app-config.yaml` be commented out by default.
+- d9b3753f87: Updated the app template to use the new `AppRouter` component instead of `app.getRouter()`, as well as `app.createRoot()` instead of `app.getProvider()`.
+
+ To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`:
+
+ ```diff
+ -import { FlatRoutes } from '@backstage/core-app-api';
+ +import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
+
+ ...
+
+ -const AppProvider = app.getProvider();
+ -const AppRouter = app.getRouter();
+
+ ...
+
+ -const App = () => (
+ +export default app.createRoot(
+ -
+ + <>
+
+
+
+ {routes}
+
+ -
+ + >,
+ );
+ ```
+
+ The final export step should end up looking something like this:
+
+ ```tsx
+ export default app.createRoot(
+ <>
+
+
+
+ {routes}
+
+ >,
+ );
+ ```
+
+ Note that `app.createRoot()` accepts a React element, rather than a component.
+
+- 71e75c0b70: Removed the `react-router` dependency from the app package, using only `react-router-dom` instead.
+
+ This change is just a bit of cleanup and is optional. If you want to apply it to your app, remove the `react-router` dependency from `packages/app/package.json`, and replace any imports from `react-router` with `react-router-dom` instead.
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.11
+
+## 0.4.35-next.4
+
+### Patch Changes
+
+- 935b66a646: Change step output template examples to use square bracket syntax.
+- dfb269fab2: Updated the template to have the `'/test'` proxy endpoint in `app-config.yaml` be commented out by default.
+- d9b3753f87: Updated the app template to use the new `AppRouter` component instead of `app.getRouter()`, as well as `app.createRoot()` instead of `app.getProvider()`.
+
+ To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`:
+
+ ```diff
+ -import { FlatRoutes } from '@backstage/core-app-api';
+ +import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
+
+ ...
+
+ -const AppProvider = app.getProvider();
+ -const AppRouter = app.getRouter();
+
+ ...
+
+ -const App = () => (
+ +export default app.createRoot(
+ -
+ + <>
+
+
+
+ {routes}
+
+ -
+ + >,
+ );
+ ```
+
+ The final export step should end up looking something like this:
+
+ ```tsx
+ export default app.createRoot(
+ <>
+
+
+
+ {routes}
+
+ >,
+ );
+ ```
+
+ Note that `app.createRoot()` accepts a React element, rather than a component.
+
+- 71e75c0b70: Removed the `react-router` dependency from the app package, using only `react-router-dom` instead.
+
+ This change is just a bit of cleanup and is optional. If you want to apply it to your app, remove the `react-router` dependency from `packages/app/package.json`, and replace any imports from `react-router` with `react-router-dom` instead.
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.11-next.0
+
+## 0.4.35-next.3
+
+### Patch Changes
+
+- c4788dbb58: Fix dependency ordering in templated packages.
+- af1358bb07: added default project name for CI job compatibility
+- Updated dependencies
+ - @backstage/cli-common@0.1.11-next.0
+
+## 0.4.35-next.2
+
+### Patch Changes
+
+- Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.11-next.0
+
+## 0.4.35-next.1
+
+### Patch Changes
+
+- Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.10
+
## 0.4.35-next.0
### Patch Changes
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index 406517cc89..83a4ef9fff 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.4.35-next.0",
+ "version": "0.4.35",
"publishConfig": {
"access": "public"
},
diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts
index f4c94d337a..f455efa6e6 100644
--- a/packages/create-app/src/createApp.test.ts
+++ b/packages/create-app/src/createApp.test.ts
@@ -42,7 +42,7 @@ const moveAppMock = jest.spyOn(tasks, 'moveAppTask');
const buildAppMock = jest.spyOn(tasks, 'buildAppTask');
describe('command entrypoint', () => {
- beforeAll(() => {
+ beforeEach(() => {
mockFs({
[`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)`
'templates/': mockFs.load(path.resolve(__dirname, '../templates/')),
diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts
index 6267081c8b..3e66b9801d 100644
--- a/packages/create-app/src/createApp.ts
+++ b/packages/create-app/src/createApp.ts
@@ -42,6 +42,7 @@ export default async (opts: OptionValues): Promise => {
{
type: 'input',
name: 'name',
+ default: 'backstage',
message: chalk.blue('Enter a name for the app [required]'),
validate: (value: any) => {
if (!value) {
diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs
index 1a45d4015b..3b5de8baa3 100644
--- a/packages/create-app/templates/default-app/app-config.yaml.hbs
+++ b/packages/create-app/templates/default-app/app-config.yaml.hbs
@@ -46,9 +46,11 @@ integrations:
# token: ${GHE_TOKEN}
proxy:
- '/test':
- target: 'https://example.com'
- changeOrigin: true
+ ### Example for how to add a proxy endpoint for the frontend.
+ ### A typical reason to do this is to handle HTTPS and CORS for internal services.
+ # '/test':
+ # target: 'https://example.com'
+ # changeOrigin: true
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
diff --git a/packages/create-app/templates/default-app/examples/template/template.yaml b/packages/create-app/templates/default-app/examples/template/template.yaml
index 50052b7a7c..33f262b49c 100644
--- a/packages/create-app/templates/default-app/examples/template/template.yaml
+++ b/packages/create-app/templates/default-app/examples/template/template.yaml
@@ -61,14 +61,14 @@ spec:
name: Register
action: catalog:register
input:
- repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
+ repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
# Outputs are displayed to the user after a successful execution of the template.
output:
links:
- title: Repository
- url: ${{ steps.publish.output.remoteUrl }}
+ url: ${{ steps['publish'].output.remoteUrl }}
- title: Open in catalog
icon: catalog
- entityRef: ${{ steps.register.output.entityRef }}
+ entityRef: ${{ steps['register'].output.entityRef }}
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index 070cb871bc..05c2436933 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -34,9 +34,9 @@
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^6.0.0",
"lerna": "^4.0.0",
+ "node-gyp": "^9.0.0",
"prettier": "^2.3.2",
- "typescript": "~4.6.4",
- "node-gyp": "^9.0.0"
+ "typescript": "~4.6.4"
},
"resolutions": {
"@types/react": "^17",
diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs
index c060cb330c..306c7c047d 100644
--- a/packages/create-app/templates/default-app/packages/app/package.json.hbs
+++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs
@@ -39,8 +39,8 @@
"@backstage/plugin-search-react": "^{{version '@backstage/plugin-search-react'}}",
"@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}",
"@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}",
- "@backstage/plugin-techdocs-react": "^{{version '@backstage/plugin-techdocs-react'}}",
"@backstage/plugin-techdocs-module-addons-contrib": "^{{version '@backstage/plugin-techdocs-module-addons-contrib'}}",
+ "@backstage/plugin-techdocs-react": "^{{version '@backstage/plugin-techdocs-react'}}",
"@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}",
"@backstage/theme": "^{{version '@backstage/theme'}}",
"@material-ui/core": "^4.12.2",
@@ -48,7 +48,6 @@
"history": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4"
},
diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx
index 46cb786399..056402f2a8 100644
--- a/packages/create-app/templates/default-app/packages/app/src/App.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Navigate, Route } from 'react-router';
+import { Navigate, Route } from 'react-router-dom';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
CatalogEntityPage,
@@ -29,7 +29,7 @@ import { Root } from './components/Root';
import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
import { createApp } from '@backstage/app-defaults';
-import { FlatRoutes } from '@backstage/core-app-api';
+import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
@@ -53,9 +53,6 @@ const app = createApp({
},
});
-const AppProvider = app.getProvider();
-const AppRouter = app.getRouter();
-
const routes = (
} />
@@ -97,14 +94,12 @@ const routes = (
);
-const App = () => (
-
+export default app.createRoot(
+ <>
{routes}
-
+ >,
);
-
-export default App;
diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs
index 1b9b79b683..ca644265b8 100644
--- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs
+++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs
@@ -16,11 +16,10 @@
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
- "app": "link:../app",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
"@backstage/backend-tasks": "^{{version '@backstage/backend-tasks'}}",
- "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
"@backstage/catalog-client": "^{{version '@backstage/catalog-client'}}",
+ "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
"@backstage/config": "^{{version '@backstage/config'}}",
"@backstage/plugin-app-backend": "^{{version '@backstage/plugin-app-backend'}}",
"@backstage/plugin-auth-backend": "^{{version '@backstage/plugin-auth-backend'}}",
@@ -34,6 +33,7 @@
"@backstage/plugin-search-backend-module-pg": "^{{version '@backstage/plugin-search-backend-module-pg'}}",
"@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}",
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
+ "app": "link:../app",
"better-sqlite3": "^7.5.0",
"dockerode": "^3.3.1",
"express": "^4.17.1",
@@ -44,8 +44,8 @@
"devDependencies": {
"@backstage/cli": "^{{version '@backstage/cli'}}",
"@types/dockerode": "^3.3.0",
- "@types/express-serve-static-core": "^4.17.5",
"@types/express": "^4.17.6",
+ "@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4"
},
"files": [
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index dec04df678..f75de52dcf 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,97 @@
# @backstage/dev-utils
+## 1.0.10
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.2
+ - @backstage/app-defaults@1.0.10
+ - @backstage/integration-react@1.1.8
+ - @backstage/plugin-catalog-react@1.2.3
+
+## 1.0.9
+
+### Patch Changes
+
+- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`.
+- 19356df560: Updated dependency `zen-observable` to `^0.9.0`.
+- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`.
+- 8015ff1258: Tweaked wording to use inclusive terminology
+- Updated dependencies
+ - @backstage/core-plugin-api@1.2.0
+ - @backstage/core-components@0.12.1
+ - @backstage/core-app-api@1.3.0
+ - @backstage/test-utils@1.2.3
+ - @backstage/app-defaults@1.0.9
+ - @backstage/plugin-catalog-react@1.2.2
+ - @backstage/integration-react@1.1.7
+ - @backstage/catalog-model@1.1.4
+ - @backstage/theme@0.2.16
+
+## 1.0.9-next.4
+
+### Patch Changes
+
+- 2e701b3796: Internal refactor to use `react-router-dom` rather than `react-router`.
+- Updated dependencies
+ - @backstage/core-app-api@1.3.0-next.4
+ - @backstage/core-components@0.12.1-next.4
+ - @backstage/app-defaults@1.0.9-next.4
+ - @backstage/test-utils@1.2.3-next.4
+ - @backstage/plugin-catalog-react@1.2.2-next.4
+ - @backstage/catalog-model@1.1.4-next.1
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/integration-react@1.1.7-next.4
+ - @backstage/theme@0.2.16
+
+## 1.0.9-next.3
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.2.1-next.3
+ - @backstage/core-components@0.12.1-next.3
+ - @backstage/app-defaults@1.0.9-next.3
+ - @backstage/catalog-model@1.1.4-next.1
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/integration-react@1.1.7-next.3
+ - @backstage/test-utils@1.2.3-next.3
+ - @backstage/theme@0.2.16
+ - @backstage/plugin-catalog-react@1.2.2-next.3
+
+## 1.0.9-next.2
+
+### Patch Changes
+
+- 8015ff1258: Tweaked wording to use inclusive terminology
+- Updated dependencies
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/core-app-api@1.2.1-next.2
+ - @backstage/core-components@0.12.1-next.2
+ - @backstage/plugin-catalog-react@1.2.2-next.2
+ - @backstage/test-utils@1.2.3-next.2
+ - @backstage/app-defaults@1.0.9-next.2
+ - @backstage/integration-react@1.1.7-next.2
+ - @backstage/catalog-model@1.1.4-next.1
+ - @backstage/theme@0.2.16
+
+## 1.0.9-next.1
+
+### Patch Changes
+
+- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`.
+- Updated dependencies
+ - @backstage/core-app-api@1.2.1-next.1
+ - @backstage/core-components@0.12.1-next.1
+ - @backstage/test-utils@1.2.3-next.1
+ - @backstage/core-plugin-api@1.1.1-next.1
+ - @backstage/plugin-catalog-react@1.2.2-next.1
+ - @backstage/app-defaults@1.0.9-next.1
+ - @backstage/integration-react@1.1.7-next.1
+ - @backstage/catalog-model@1.1.4-next.1
+ - @backstage/theme@0.2.16
+
## 1.0.9-next.0
### Patch Changes
@@ -896,7 +988,7 @@
### Patch Changes
-- 5aa4ceea6: Make sure to provide dummy routes for all external routes of plugins given to DevApp
+- 5aa4ceea6: Make sure to provide sample routes for all external routes of plugins given to DevApp
- Updated dependencies [3a58084b6]
- Updated dependencies [e799e74d4]
- Updated dependencies [dc12852c9]
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index bbffca2956..74414da5c3 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "1.0.9-next.0",
+ "version": "1.0.9",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -47,13 +47,12 @@
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"react-use": "^17.2.4",
- "zen-observable": "^0.9.0"
+ "zen-observable": "^0.10.0"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
"react": "^16.13.1 || ^17.0.0",
"react-dom": "^16.13.1 || ^17.0.0",
- "react-router": "6.0.0-beta.0 || ^6.3.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
diff --git a/packages/dev-utils/src/devApp/render.test.tsx b/packages/dev-utils/src/devApp/render.test.tsx
index 83a24bd9b2..2da6ef370a 100644
--- a/packages/dev-utils/src/devApp/render.test.tsx
+++ b/packages/dev-utils/src/devApp/render.test.tsx
@@ -24,7 +24,12 @@ const anyEnv = (process.env = { ...process.env }) as any;
describe('DevAppBuilder', () => {
it('should be able to render a component in a dev app', async () => {
anyEnv.APP_CONFIG = [
- { context: 'test', data: { app: { title: 'Test App' } } },
+ {
+ context: 'test',
+ data: {
+ app: { title: 'Test App' },
+ },
+ },
];
const MyComponent = () => {
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index e04779b4d1..d2c91c9394 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -46,7 +46,7 @@ import { Box } from '@material-ui/core';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import React, { ComponentType, ReactNode } from 'react';
import ReactDOM from 'react-dom';
-import { createRoutesFromChildren, Route } from 'react-router';
+import { createRoutesFromChildren, Route } from 'react-router-dom';
import { SidebarThemeSwitcher } from './SidebarThemeSwitcher';
export function isReactRouterBeta(): boolean {
@@ -165,9 +165,9 @@ export class DevAppBuilder {
* Build a DevApp component using the resources registered so far
*/
build(): ComponentType<{}> {
- const dummyRouteRef = createRouteRef({ id: 'dummy' });
- const DummyPage = () => Page belonging to another plugin.;
- attachComponentData(DummyPage, 'core.mountPoint', dummyRouteRef);
+ const fakeRouteRef = createRouteRef({ id: 'fake' });
+ const FakePage = () => Page belonging to another plugin.;
+ attachComponentData(FakePage, 'core.mountPoint', fakeRouteRef);
const apis = [...this.apis];
if (!apis.some(api => api.api.id === scmIntegrationsApiRef.id)) {
@@ -188,7 +188,7 @@ export class DevAppBuilder {
for (const plugin of this.plugins ?? []) {
const targets: Record> = {};
for (const routeKey of Object.keys(plugin.externalRoutes)) {
- targets[routeKey] = dummyRouteRef;
+ targets[routeKey] = fakeRouteRef;
}
bind(plugin.externalRoutes, targets);
}
@@ -215,7 +215,7 @@ export class DevAppBuilder {
{this.routes}
- } />
+ } />
diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts
index fe4d87bea4..3eedb90b21 100644
--- a/packages/e2e-test/src/commands/run.ts
+++ b/packages/e2e-test/src/commands/run.ts
@@ -57,13 +57,14 @@ export async function run() {
const appDir = await createApp('test-app', workspaceDir, rootDir);
print('Creating a Backstage Plugin');
- const pluginName = await createPlugin('test-plugin', appDir);
+ const pluginId = 'test';
+ await createPlugin({ appDir, pluginId, select: 'plugin' });
print('Creating a Backstage Backend Plugin');
- await createPlugin('test-plugin', appDir, ['--backend']);
+ await createPlugin({ appDir, pluginId, select: 'backend-plugin' });
print('Starting the app');
- await testAppServe(pluginName, appDir);
+ await testAppServe(pluginId, appDir);
if (Boolean(process.env.POSTGRES_USER)) {
print('Testing the PostgreSQL backend startup');
@@ -328,14 +329,18 @@ async function overrideModuleResolutions(appDir: string, workspaceDir: string) {
/**
* Uses create-plugin command to create a new plugin in the app
*/
-async function createPlugin(
- pluginName: string,
- appDir: string,
- options: string[] = [],
-) {
- const child = spawnPiped(['yarn', 'create-plugin', ...options], {
- cwd: appDir,
- });
+async function createPlugin(options: {
+ appDir: string;
+ pluginId: string;
+ select: string;
+}) {
+ const { appDir, pluginId, select } = options;
+ const child = spawnPiped(
+ ['yarn', 'new', '--select', select, '--option', `id=${pluginId}`],
+ {
+ cwd: appDir,
+ },
+ );
try {
let stdout = '';
@@ -343,20 +348,14 @@ async function createPlugin(
stdout = stdout + data.toString('utf8');
});
- await waitFor(() => stdout.includes('Enter an ID for the plugin'));
- child.stdin?.write(`${pluginName}\n`);
-
- // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin'));
- // child.stdin.write('@someuser\n');
-
print('Waiting for plugin create script to be done');
await waitForExit(child);
- const canonicalName = options.includes('--backend')
- ? `${pluginName}-backend`
- : pluginName;
-
- const pluginDir = resolvePath(appDir, 'plugins', canonicalName);
+ const pluginDir = resolvePath(
+ appDir,
+ 'plugins',
+ select === 'backend-plugin' ? `${pluginId}-backend` : pluginId,
+ );
print(`Running 'yarn tsc' in root for newly created plugin`);
await runPlain(['yarn', 'tsc'], { cwd: appDir });
@@ -365,8 +364,6 @@ async function createPlugin(
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
}
-
- return canonicalName;
} finally {
child.kill();
}
@@ -375,7 +372,7 @@ async function createPlugin(
/**
* Start serving the newly created app and make sure that the create plugin is rendering correctly
*/
-async function testAppServe(pluginName: string, appDir: string) {
+async function testAppServe(pluginId: string, appDir: string) {
const startApp = spawnPiped(['yarn', 'start'], {
cwd: appDir,
env: {
@@ -398,8 +395,8 @@ async function testAppServe(pluginName: string, appDir: string) {
await waitForPageWithText(page, '/', 'My Company Catalog');
await waitForPageWithText(
page,
- `/${pluginName}`,
- `Welcome to ${pluginName}!`,
+ `/${pluginId}`,
+ `Welcome to ${pluginId}!`,
);
print('Both App and Plugin loaded correctly');
diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md
index 7cc653877a..e7803e2fb4 100644
--- a/packages/errors/CHANGELOG.md
+++ b/packages/errors/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/errors
+## 1.1.4
+
+### Patch Changes
+
+- ac6cc9f7bd: Removed a circular import
+- Updated dependencies
+ - @backstage/types@1.0.2
+
+## 1.1.4-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/types@1.0.2-next.1
+
## 1.1.4-next.0
### Patch Changes
diff --git a/packages/errors/package.json b/packages/errors/package.json
index d9d7175890..1b9b865b74 100644
--- a/packages/errors/package.json
+++ b/packages/errors/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/errors",
"description": "Common utilities for error handling within Backstage",
- "version": "1.1.4-next.0",
+ "version": "1.1.4",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts
index 786e10ef23..b831d4eea3 100644
--- a/packages/errors/src/errors/CustomErrorBase.ts
+++ b/packages/errors/src/errors/CustomErrorBase.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { stringifyError } from '../serialization';
+import { stringifyError } from '../serialization/error';
import { isError } from './assertion';
/**
diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts
index a26891ef6a..f8ba0a1ba1 100644
--- a/packages/errors/src/errors/ResponseError.ts
+++ b/packages/errors/src/errors/ResponseError.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { deserializeError } from '../serialization';
+import { deserializeError } from '../serialization/error';
import {
ErrorResponseBody,
parseErrorResponseBody,
diff --git a/packages/errors/src/serialization/error.ts b/packages/errors/src/serialization/error.ts
index 8d031949bf..e48309f1e7 100644
--- a/packages/errors/src/serialization/error.ts
+++ b/packages/errors/src/serialization/error.ts
@@ -19,7 +19,7 @@ import {
deserializeError as deserializeErrorInternal,
serializeError as serializeErrorInternal,
} from 'serialize-error';
-import { isError } from '../errors';
+import { isError } from '../errors/assertion';
/**
* The serialized form of an Error.
diff --git a/packages/integration-aws-node/.eslintrc.js b/packages/integration-aws-node/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/packages/integration-aws-node/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md
new file mode 100644
index 0000000000..d07b6ab8aa
--- /dev/null
+++ b/packages/integration-aws-node/CHANGELOG.md
@@ -0,0 +1,25 @@
+# @backstage/integration-aws-node
+
+## 0.1.0
+
+### Minor Changes
+
+- 13278732f6: New package for AWS integration node library
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/errors@1.1.4
+ - @backstage/config@1.0.5
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 13278732f6: New package for AWS integration node library
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.5-next.1
+ - @backstage/errors@1.1.4-next.1
diff --git a/packages/integration-aws-node/README.md b/packages/integration-aws-node/README.md
new file mode 100644
index 0000000000..62df26756f
--- /dev/null
+++ b/packages/integration-aws-node/README.md
@@ -0,0 +1,162 @@
+# @backstage/integration-aws-node
+
+This package providers helpers for fetching AWS account credentials
+to be used by AWS SDK clients in backend packages and plugins.
+
+## Backstage app configuration
+
+Users of plugins and packages that use this library
+will configure their AWS account information and credentials in their
+Backstage app config.
+Users can configure IAM user credentials, IAM roles, and profile names
+for their AWS accounts in their Backstage config.
+
+If the AWS integration configuration is missing, the credentials manager
+from this package will fall back to the AWS SDK default credentials chain for
+resources in the main AWS account.
+The default credentials chain for Node resolves credentials in the
+following order of precedence:
+
+1. Environment variables
+2. SSO credentials from token cache
+3. Web identity token credentials
+4. Shared credentials files
+5. The EC2/ECS Instance Metadata Service
+
+See more about the AWS SDK default credentials chain in the
+[AWS SDK for Javascript Developer Guide](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html).
+
+Configuration examples:
+
+```yaml
+aws:
+ # The main account is used as the source of credentials for calling
+ # the STS AssumeRole API to assume IAM roles in other AWS accounts.
+ # This section can be omitted to fall back to the AWS SDK's default creds chain.
+ mainAccount:
+ accessKeyId: ${MY_ACCESS_KEY_ID}
+ secretAccessKey: ${MY_SECRET_ACCESS_KEY}
+
+ # Account credentials can be configured individually per account
+ accounts:
+ # Credentials can come from a role in the account
+ - accountId: '111111111111'
+ roleName: 'my-iam-role-name'
+ externalId: 'my-external-id'
+
+ # Credentials can come from other AWS partitions
+ - accountId: '222222222222'
+ partition: 'aws-other'
+ roleName: 'my-iam-role-name'
+ # The STS region to use for the AssumeRole call
+ region: 'not-us-east-1'
+ # The creds to use when calling AssumeRole
+ accessKeyId: ${MY_ACCESS_KEY_ID_FOR_ANOTHER_PARTITION}
+ secretAccessKey: ${MY_SECRET_ACCESS_KEY_FOR_ANOTHER_PARTITION}
+
+ # Credentials can come from static credentials
+ - accountId: '333333333333'
+ accessKeyId: ${MY_OTHER_ACCESS_KEY_ID}
+ secretAccessKey: ${MY_OTHER_SECRET_ACCESS_KEY}
+
+ # Credentials can come from a profile in a shared config file on disk
+ - accountId: '444444444444'
+ profile: my-profile-name
+
+ # Credentials can come from the AWS SDK's default creds chain
+ - accountId: '555555555555'
+
+ # Credentials for accounts can fall back to a common role name.
+ # This is useful for account discovery use cases where the account
+ # IDs may not be known when writing the static config.
+ # If all accounts have a role with the same name, then the "accounts"
+ # section can be omitted entirely.
+ accountDefaults:
+ roleName: 'my-backstage-role'
+ externalId: 'my-id'
+```
+
+## Integrate new plugins
+
+Backend plugins can provide an AWS ARN or account ID to this library in order to
+retrieve a credential provider for the relevant account that can be fed directly
+to an AWS SDK client.
+The AWS SDK for Javascript V3 must be used.
+
+```typescript
+const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
+
+// provide the account ID explicitly
+const credProvider = await awsCredentialsManager.getCredentialProvider({
+ accountId,
+});
+// OR extract the account ID from the ARN
+const credProvider = await awsCredentialsManager.getCredentialProvider({ arn });
+// OR provide neither to get main account's credentials
+const credProvider = await awsCredentialsManager.getCredentialProvider({});
+
+// Example constructing an AWS Proton client with the returned credential provider
+const client = new ProtonClient({
+ region,
+ credentialDefaultProvider: () => credProvider.sdkCredentialProvider,
+});
+```
+
+Depending on the nature of your plugin, you may either have the user specify the
+relevant ARN or account ID in a catalog entity annotation or in the static Backstage
+app configuration for your plugin.
+
+For example, you can create a new catalog entity annotation for your plugin containing
+either an AWS account ID or ARN:
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ annotations:
+ # Plugin annotation to specify an AWS account ID
+ my-plugin.io/aws-account-id: '123456789012'
+ # Plugin annotation to specify the AWS ARN of a specific resource
+ my-other-plugin.io/aws-dynamodb-table: 'arn:aws:dynamodb:us-east-2:123456789012:table/example-table'
+```
+
+In your plugin, read the annotation value so that you can retrieve the credential provider:
+
+```typescript
+const MY_AWS_ACCOUNT_ID_ANNOTATION = 'my-plugin.io/aws-account-id';
+
+const getAwsAccountId = (entity: Entity) =>
+ entity.metadata.annotations?.[MY_AWS_ACCOUNT_ID_ANNOTATION]);
+```
+
+Alternatively, you can create a new Backstage app configuration field for your plugin:
+
+```yaml
+# app-config.yaml
+my-plugin:
+ # Statically configure the AWS account ID to use
+ awsAccountId: '123456789012'
+my-other-plugin:
+ # Statically configure the AWS ARN of a specific resource
+ awsDynamoDbTable: 'arn:aws:dynamodb:us-east-2:123456789012:table/example-table'
+```
+
+In your plugin, read the configuration value so that you can retrieve the credential provider:
+
+```typescript
+// Read an account ID from your plugin's configuration
+const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
+const accountId = config.getOptionalString('my-plugin.awsAccountId');
+const credProvider = await awsCredentialsManager.getCredentialProvider({
+ accountId,
+});
+
+// Or, read an AWS ARN from your plugin's configuration
+const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config);
+const arn = config.getString('my-other-plugin.awsDynamoDbTable');
+const credProvider = await awsCredentialsManager.getCredentialProvider({ arn });
+```
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/packages/integration-aws-node/api-report.md b/packages/integration-aws-node/api-report.md
new file mode 100644
index 0000000000..d9ed25e193
--- /dev/null
+++ b/packages/integration-aws-node/api-report.md
@@ -0,0 +1,39 @@
+## API Report File for "@backstage/integration-aws-node"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
+import { Config } from '@backstage/config';
+
+// @public
+export type AwsCredentialProvider = {
+ accountId?: string;
+ stsRegion?: string;
+ sdkCredentialProvider: AwsCredentialIdentityProvider;
+};
+
+// @public
+export type AwsCredentialProviderOptions = {
+ accountId?: string;
+ arn?: string;
+};
+
+// @public
+export interface AwsCredentialsManager {
+ getCredentialProvider(
+ opts?: AwsCredentialProviderOptions,
+ ): Promise;
+}
+
+// @public
+export class DefaultAwsCredentialsManager implements AwsCredentialsManager {
+ // (undocumented)
+ static fromConfig(config: Config): DefaultAwsCredentialsManager;
+ getCredentialProvider(
+ opts?: AwsCredentialProviderOptions,
+ ): Promise;
+}
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/packages/integration-aws-node/config.d.ts b/packages/integration-aws-node/config.d.ts
new file mode 100644
index 0000000000..3c5600efc3
--- /dev/null
+++ b/packages/integration-aws-node/config.d.ts
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface Config {
+ /** Configuration for access to AWS accounts */
+ aws?: {
+ /**
+ * Defaults for retrieving AWS account credentials
+ */
+ accountDefaults?: {
+ /**
+ * The IAM role to assume to retrieve temporary AWS credentials
+ */
+ roleName?: string;
+
+ /**
+ * The AWS partition of the IAM role, e.g. "aws", "aws-cn"
+ */
+ partition?: string;
+
+ /**
+ * The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
+ */
+ region?: string;
+
+ /**
+ * The unique identifier needed to assume the role to retrieve temporary AWS credentials
+ * @visibility secret
+ */
+ externalId?: string;
+ };
+
+ /**
+ * Main account to use for retrieving AWS account credentials
+ */
+ mainAccount?: {
+ /**
+ * The access key ID for a set of static AWS credentials
+ * @visibility secret
+ */
+ accessKeyId?: string;
+
+ /**
+ * The secret access key for a set of static AWS credentials
+ * @visibility secret
+ */
+ secretAccessKey?: string;
+
+ /**
+ * The configuration profile from a credentials file at ~/.aws/credentials and
+ * a configuration file at ~/.aws/config.
+ */
+ profile?: string;
+
+ /**
+ * The STS regional endpoint to use for the main account, e.g. "ap-northeast-1"
+ */
+ region?: string;
+ };
+
+ /**
+ * Configuration for retrieving AWS accounts credentials
+ */
+ accounts?: Array<{
+ /**
+ * The account ID of the target account that this matches on, e.g. "123456789012"
+ */
+ accountId: string;
+
+ /**
+ * The access key ID for a set of static AWS credentials
+ * @visibility secret
+ */
+ accessKeyId?: string;
+
+ /**
+ * The secret access key for a set of static AWS credentials
+ * @visibility secret
+ */
+ secretAccessKey?: string;
+
+ /**
+ * The configuration profile from a credentials file at ~/.aws/credentials and
+ * a configuration file at ~/.aws/config.
+ */
+ profile?: string;
+
+ /**
+ * The IAM role to assume to retrieve temporary AWS credentials
+ */
+ roleName?: string;
+
+ /**
+ * The AWS partition of the IAM role, e.g. "aws", "aws-cn"
+ */
+ partition?: string;
+
+ /**
+ * The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
+ */
+ region?: string;
+
+ /**
+ * The unique identifier needed to assume the role to retrieve temporary AWS credentials
+ * @visibility secret
+ */
+ externalId?: string;
+ }>;
+ };
+}
diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json
new file mode 100644
index 0000000000..a5b0ed08bb
--- /dev/null
+++ b/packages/integration-aws-node/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@backstage/integration-aws-node",
+ "description": "Helpers for fetching AWS account credentials",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "module": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "node-library"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "packages/integration-aws-node"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@aws-sdk/client-sts": "^3.208.0",
+ "@aws-sdk/credential-provider-node": "^3.208.0",
+ "@aws-sdk/credential-providers": "^3.208.0",
+ "@aws-sdk/types": "^3.208.0",
+ "@aws-sdk/util-arn-parser": "^3.208.0",
+ "@backstage/config": "workspace:^",
+ "@backstage/errors": "workspace:^"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@backstage/config-loader": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
+ "aws-sdk-client-mock": "^2.0.0",
+ "aws-sdk-client-mock-jest": "^2.0.0"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts
new file mode 100644
index 0000000000..1b21d43338
--- /dev/null
+++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts
@@ -0,0 +1,430 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DefaultAwsCredentialsManager } from './DefaultAwsCredentialsManager';
+import { mockClient, AwsClientStub } from 'aws-sdk-client-mock';
+import 'aws-sdk-client-mock-jest';
+import {
+ STSClient,
+ GetCallerIdentityCommand,
+ AssumeRoleCommand,
+} from '@aws-sdk/client-sts';
+import { Config, ConfigReader } from '@backstage/config';
+import { promises } from 'fs';
+
+const env = process.env;
+let stsMock: AwsClientStub;
+let config: Config;
+
+jest.mock('fs', () => ({ promises: { readFile: jest.fn() } }));
+
+describe('DefaultAwsCredentialsManager', () => {
+ beforeEach(() => {
+ process.env = { ...env };
+ jest.resetAllMocks();
+
+ stsMock = mockClient(STSClient);
+
+ config = new ConfigReader({
+ aws: {
+ accounts: [
+ {
+ accountId: '111111111111',
+ roleName: 'hello',
+ externalId: 'world',
+ },
+ {
+ accountId: '222222222222',
+ roleName: 'hi',
+ partition: 'aws-other',
+ region: 'not-us-east-1',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ },
+ {
+ accountId: '333333333333',
+ accessKeyId: 'my-access-key',
+ secretAccessKey: 'my-secret-access-key',
+ },
+ {
+ accountId: '444444444444',
+ },
+ {
+ accountId: '555555555555',
+ profile: 'my-profile',
+ },
+ ],
+ accountDefaults: {
+ roleName: 'backstage-role',
+ externalId: 'my-id',
+ },
+ mainAccount: {
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ region: 'ap-northeast-1',
+ },
+ },
+ });
+
+ stsMock.on(GetCallerIdentityCommand).resolvesOnce({
+ Account: '123456789012',
+ });
+
+ stsMock
+ .on(AssumeRoleCommand, {
+ RoleArn: 'arn:aws:iam::111111111111:role/hello',
+ RoleSessionName: 'backstage',
+ ExternalId: 'world',
+ })
+ .resolves({
+ Credentials: {
+ AccessKeyId: 'ACCESS_KEY_ID_1',
+ SecretAccessKey: 'SECRET_ACCESS_KEY_1',
+ SessionToken: 'SESSION_TOKEN_1',
+ Expiration: new Date('2022-01-01'),
+ },
+ });
+
+ stsMock
+ .on(AssumeRoleCommand, {
+ RoleArn: 'arn:aws-other:iam::222222222222:role/hi',
+ RoleSessionName: 'backstage',
+ })
+ .resolves({
+ Credentials: {
+ AccessKeyId: 'ACCESS_KEY_ID_2',
+ SecretAccessKey: 'SECRET_ACCESS_KEY_2',
+ SessionToken: 'SESSION_TOKEN_2',
+ Expiration: new Date('2022-01-02'),
+ },
+ });
+
+ stsMock
+ .on(AssumeRoleCommand, {
+ RoleArn: 'arn:aws:iam::999999999999:role/backstage-role',
+ RoleSessionName: 'backstage',
+ ExternalId: 'my-id',
+ })
+ .resolves({
+ Credentials: {
+ AccessKeyId: 'ACCESS_KEY_ID_9',
+ SecretAccessKey: 'SECRET_ACCESS_KEY_9',
+ SessionToken: 'SESSION_TOKEN_9',
+ Expiration: new Date('2022-01-09'),
+ },
+ });
+
+ process.env.AWS_ACCESS_KEY_ID = 'ACCESS_KEY_ID_10';
+ process.env.AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_10';
+ process.env.AWS_SESSION_TOKEN = 'SESSION_TOKEN_10';
+ process.env.AWS_CREDENTIAL_EXPIRATION = new Date(
+ '2022-01-10',
+ ).toISOString();
+
+ const mockProfile = `[my-profile]
+ aws_access_key_id=ACCESS_KEY_ID_9
+ aws_secret_access_key=SECRET_ACCESS_KEY_9
+ `;
+ (promises.readFile as jest.Mock).mockResolvedValue(mockProfile);
+ });
+
+ afterEach(() => {
+ process.env = env;
+ });
+
+ describe('#getCredentialProvider', () => {
+ it('retrieves assume-role creds for the given account ID and caches the provider', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '111111111111',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('111111111111');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_1',
+ secretAccessKey: 'SECRET_ACCESS_KEY_1',
+ sessionToken: 'SESSION_TOKEN_1',
+ expiration: new Date('2022-01-01'),
+ });
+
+ const awsCredentialProvider2 = await provider.getCredentialProvider({
+ accountId: '111111111111',
+ });
+
+ expect(awsCredentialProvider).toBe(awsCredentialProvider2);
+ expect(stsMock).toHaveReceivedCommandTimes(AssumeRoleCommand, 1);
+ });
+
+ it('retrieves assume-role creds in another partition for the given account ID', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '222222222222',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('222222222222');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_2',
+ secretAccessKey: 'SECRET_ACCESS_KEY_2',
+ sessionToken: 'SESSION_TOKEN_2',
+ expiration: new Date('2022-01-02'),
+ });
+ });
+
+ it('retrieves assume-role creds for an account using the account defaults', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '999999999999',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('999999999999');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_9',
+ secretAccessKey: 'SECRET_ACCESS_KEY_9',
+ sessionToken: 'SESSION_TOKEN_9',
+ expiration: new Date('2022-01-09'),
+ });
+ });
+
+ it('retrieves static creds for the given account ID', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '333333333333',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('333333333333');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'my-access-key',
+ secretAccessKey: 'my-secret-access-key',
+ });
+ });
+
+ it('retrieves static creds from the main account', async () => {
+ const minConfig = new ConfigReader({
+ aws: {
+ mainAccount: {
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ },
+ },
+ });
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '123456789012',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ });
+ });
+
+ it('only queries the main account ID once from STS', async () => {
+ const minConfig = new ConfigReader({
+ aws: {
+ mainAccount: {
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ },
+ },
+ });
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ const awsCredentialProvider1 = await provider.getCredentialProvider({});
+ const awsCredentialProvider2 = await provider.getCredentialProvider({});
+
+ expect(awsCredentialProvider1).toBe(awsCredentialProvider2);
+ expect(stsMock).toHaveReceivedCommandTimes(GetCallerIdentityCommand, 1);
+ });
+
+ it('retrieves the ini provider chain for the given account ID', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '555555555555',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('555555555555');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_9',
+ secretAccessKey: 'SECRET_ACCESS_KEY_9',
+ });
+ });
+
+ it('retrieves the default cred provider chain for the given account ID', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '444444444444',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('444444444444');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_10',
+ secretAccessKey: 'SECRET_ACCESS_KEY_10',
+ sessionToken: 'SESSION_TOKEN_10',
+ expiration: new Date('2022-01-10'),
+ });
+ });
+
+ it('retrieves ini provider chain from the main account', async () => {
+ const minConfig = new ConfigReader({
+ aws: {
+ mainAccount: {
+ profile: 'my-profile',
+ },
+ },
+ });
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '123456789012',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_9',
+ secretAccessKey: 'SECRET_ACCESS_KEY_9',
+ });
+ });
+
+ it('retrieves default cred provider chain from the main account', async () => {
+ const minConfig = new ConfigReader({
+ aws: {},
+ });
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '123456789012',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_10',
+ secretAccessKey: 'SECRET_ACCESS_KEY_10',
+ sessionToken: 'SESSION_TOKEN_10',
+ expiration: new Date('2022-01-10'),
+ });
+ });
+
+ it('retrieves default cred provider chain from the main account when there is no AWS integration config', async () => {
+ const minConfig = new ConfigReader({});
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ accountId: '123456789012',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_10',
+ secretAccessKey: 'SECRET_ACCESS_KEY_10',
+ sessionToken: 'SESSION_TOKEN_10',
+ expiration: new Date('2022-01-10'),
+ });
+ });
+
+ it('extracts the account ID from an ARN', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ arn: 'arn:aws:ecs:region:111111111111:service/cluster-name/service-name',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('111111111111');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'ACCESS_KEY_ID_1',
+ secretAccessKey: 'SECRET_ACCESS_KEY_1',
+ sessionToken: 'SESSION_TOKEN_1',
+ expiration: new Date('2022-01-01'),
+ });
+ });
+
+ it('falls back to main account credentials when account ID cannot be extracted from the ARN', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({
+ arn: 'arn:aws:s3:::bucket_name',
+ });
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ });
+ });
+
+ it('falls back to main account credentials when neither account ID nor ARN are provided', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider({});
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ });
+ });
+
+ it('falls back to main account credentials when no options are provided', async () => {
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ const awsCredentialProvider = await provider.getCredentialProvider();
+
+ expect(awsCredentialProvider.accountId).toEqual('123456789012');
+
+ const creds = await awsCredentialProvider.sdkCredentialProvider();
+ expect(creds).toEqual({
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ });
+ });
+
+ it('rejects account that is not configured, with no account defaults', async () => {
+ const minConfig = new ConfigReader({
+ aws: {},
+ });
+ const provider = DefaultAwsCredentialsManager.fromConfig(minConfig);
+ await expect(
+ provider.getCredentialProvider({ accountId: '111222333444' }),
+ ).rejects.toThrow(/no AWS integration that matches 111222333444/);
+ });
+
+ it('rejects main account that has invalid credentials', async () => {
+ stsMock.on(GetCallerIdentityCommand).rejects('No credentials found');
+ const provider = DefaultAwsCredentialsManager.fromConfig(config);
+ await expect(provider.getCredentialProvider({})).rejects.toThrow(
+ /No credentials found/,
+ );
+ });
+ });
+});
diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts
new file mode 100644
index 0000000000..7799f08b4d
--- /dev/null
+++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.ts
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ readAwsIntegrationConfig,
+ AwsIntegrationAccountConfig,
+ AwsIntegrationDefaultAccountConfig,
+ AwsIntegrationMainAccountConfig,
+} from './config';
+import {
+ AwsCredentialsManager,
+ AwsCredentialProvider,
+ AwsCredentialProviderOptions,
+} from './types';
+import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
+import {
+ fromIni,
+ fromNodeProviderChain,
+ fromTemporaryCredentials,
+} from '@aws-sdk/credential-providers';
+import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
+import { parse } from '@aws-sdk/util-arn-parser';
+import { Config } from '@backstage/config';
+
+/**
+ * Retrieves the account ID for the given credential provider from STS.
+ */
+async function fillInAccountId(credProvider: AwsCredentialProvider) {
+ if (credProvider.accountId) {
+ return;
+ }
+
+ const client = new STSClient({
+ region: credProvider.stsRegion,
+ customUserAgent: 'backstage-aws-credentials-manager',
+ credentialDefaultProvider: () => credProvider.sdkCredentialProvider,
+ });
+ const resp = await client.send(new GetCallerIdentityCommand({}));
+ credProvider.accountId = resp.Account!;
+}
+
+function getStaticCredentials(
+ accessKeyId: string,
+ secretAccessKey: string,
+): AwsCredentialIdentityProvider {
+ return async () => {
+ return Promise.resolve({
+ accessKeyId: accessKeyId,
+ secretAccessKey: secretAccessKey,
+ });
+ };
+}
+
+function getProfileCredentials(
+ profile: string,
+ region?: string,
+): AwsCredentialIdentityProvider {
+ return fromIni({
+ profile,
+ clientConfig: {
+ region,
+ customUserAgent: 'backstage-aws-credentials-manager',
+ },
+ });
+}
+
+function getDefaultCredentialsChain(): AwsCredentialIdentityProvider {
+ return fromNodeProviderChain();
+}
+
+/**
+ * Constructs the credential provider needed by the AWS SDK from the given account config
+ *
+ * Order of precedence:
+ * 1. Assume role with static creds
+ * 2. Assume role with main account creds
+ * 3. Static creds
+ * 4. Profile creds
+ * 5. Default AWS SDK creds chain
+ */
+function getSdkCredentialProvider(
+ config: AwsIntegrationAccountConfig,
+ mainAccountCredProvider: AwsCredentialIdentityProvider,
+): AwsCredentialIdentityProvider {
+ if (config.roleName) {
+ const region = config.region ?? 'us-east-1';
+ const partition = config.partition ?? 'aws';
+
+ return fromTemporaryCredentials({
+ masterCredentials: config.accessKeyId
+ ? getStaticCredentials(config.accessKeyId!, config.secretAccessKey!)
+ : mainAccountCredProvider,
+ params: {
+ RoleArn: `arn:${partition}:iam::${config.accountId}:role/${config.roleName}`,
+ RoleSessionName: 'backstage',
+ ExternalId: config.externalId,
+ },
+ clientConfig: {
+ region,
+ customUserAgent: 'backstage-aws-credentials-manager',
+ },
+ });
+ }
+
+ if (config.accessKeyId) {
+ return getStaticCredentials(config.accessKeyId!, config.secretAccessKey!);
+ }
+
+ if (config.profile) {
+ return getProfileCredentials(config.profile!, config.region);
+ }
+
+ return getDefaultCredentialsChain();
+}
+
+/**
+ * Constructs the credential provider needed by the AWS SDK for the main account
+ *
+ * Order of precedence:
+ * 1. Static creds
+ * 2. Profile creds
+ * 3. Default AWS SDK creds chain
+ */
+function getMainAccountSdkCredentialProvider(
+ config: AwsIntegrationMainAccountConfig,
+): AwsCredentialIdentityProvider {
+ if (config.accessKeyId) {
+ return getStaticCredentials(config.accessKeyId!, config.secretAccessKey!);
+ }
+
+ if (config.profile) {
+ return getProfileCredentials(config.profile!, config.region);
+ }
+
+ return getDefaultCredentialsChain();
+}
+
+/**
+ * Handles the creation and caching of credential providers for AWS accounts.
+ *
+ * @public
+ */
+export class DefaultAwsCredentialsManager implements AwsCredentialsManager {
+ static fromConfig(config: Config): DefaultAwsCredentialsManager {
+ const awsConfig = config.has('aws')
+ ? readAwsIntegrationConfig(config.getConfig('aws'))
+ : {
+ accounts: [],
+ mainAccount: {},
+ accountDefaults: {},
+ };
+
+ const mainAccountSdkCredProvider = getMainAccountSdkCredentialProvider(
+ awsConfig.mainAccount,
+ );
+ const mainAccountCredProvider: AwsCredentialProvider = {
+ sdkCredentialProvider: mainAccountSdkCredProvider,
+ };
+
+ const accountCredProviders = new Map();
+ for (const accountConfig of awsConfig.accounts) {
+ const sdkCredentialProvider = getSdkCredentialProvider(
+ accountConfig,
+ mainAccountSdkCredProvider,
+ );
+ accountCredProviders.set(accountConfig.accountId, {
+ accountId: accountConfig.accountId,
+ stsRegion: accountConfig.region,
+ sdkCredentialProvider,
+ });
+ }
+
+ return new DefaultAwsCredentialsManager(
+ accountCredProviders,
+ awsConfig.accountDefaults,
+ mainAccountCredProvider,
+ );
+ }
+
+ private constructor(
+ private readonly accountCredentialProviders: Map<
+ string,
+ AwsCredentialProvider
+ >,
+ private readonly accountDefaults: AwsIntegrationDefaultAccountConfig,
+ private readonly mainAccountCredentialProvider: AwsCredentialProvider,
+ ) {}
+
+ /**
+ * Returns an {@link AwsCredentialProvider} for a given AWS account.
+ *
+ * @example
+ * ```ts
+ * const { provider } = await getCredentialProvider({
+ * accountId: '0123456789012',
+ * })
+ *
+ * const { provider } = await getCredentialProvider({
+ * arn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'
+ * })
+ * ```
+ *
+ * @param opts - the AWS account ID or AWS resource ARN
+ * @returns A promise of {@link AwsCredentialProvider}.
+ */
+ async getCredentialProvider(
+ opts?: AwsCredentialProviderOptions,
+ ): Promise {
+ // If no options provided, fall back to the main account
+ if (!opts) {
+ await fillInAccountId(this.mainAccountCredentialProvider);
+ return this.mainAccountCredentialProvider;
+ }
+
+ // Determine the account ID: either explicitly provided or extracted from the provided ARN
+ let accountId = opts.accountId;
+ if (opts.arn && !accountId) {
+ const arnComponents = parse(opts.arn);
+ accountId = arnComponents.accountId;
+ }
+
+ // If the account ID was not provided (explicitly or in the ARN),
+ // fall back to the main account
+ if (!accountId) {
+ await fillInAccountId(this.mainAccountCredentialProvider);
+ return this.mainAccountCredentialProvider;
+ }
+
+ // Return a cached provider if available
+ if (this.accountCredentialProviders.has(accountId)) {
+ return this.accountCredentialProviders.get(accountId)!;
+ }
+
+ // First, fall back to using the account defaults
+ if (this.accountDefaults.roleName) {
+ const config: AwsIntegrationAccountConfig = {
+ accountId,
+ roleName: this.accountDefaults.roleName,
+ partition: this.accountDefaults.partition,
+ region: this.accountDefaults.region,
+ externalId: this.accountDefaults.externalId,
+ };
+ const sdkCredentialProvider = getSdkCredentialProvider(
+ config,
+ this.mainAccountCredentialProvider.sdkCredentialProvider,
+ );
+ const credProvider: AwsCredentialProvider = {
+ accountId,
+ sdkCredentialProvider,
+ };
+ this.accountCredentialProviders.set(accountId, credProvider);
+ return credProvider;
+ }
+
+ // Then, fall back to using the main account, but only
+ // if the account requested matches the main account ID
+ await fillInAccountId(this.mainAccountCredentialProvider);
+ if (accountId === this.mainAccountCredentialProvider.accountId) {
+ return this.mainAccountCredentialProvider;
+ }
+
+ // Otherwise, the account needs to be explicitly configured in Backstage
+ throw new Error(
+ `There is no AWS integration that matches ${accountId}. Please add a configuration for this AWS account.`,
+ );
+ }
+}
diff --git a/packages/integration-aws-node/src/config.test.ts b/packages/integration-aws-node/src/config.test.ts
new file mode 100644
index 0000000000..f761a8ac14
--- /dev/null
+++ b/packages/integration-aws-node/src/config.test.ts
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config, ConfigReader } from '@backstage/config';
+import { AwsIntegrationConfig, readAwsIntegrationConfig } from './config';
+
+describe('readAwsIntegrationConfig', () => {
+ function buildConfig(data: Partial): Config {
+ return new ConfigReader(data);
+ }
+
+ it('reads all values', () => {
+ const output = readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ {
+ accountId: '111111111111',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ roleName: 'hello',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'world',
+ },
+ {
+ accountId: '222222222222',
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ },
+ {
+ accountId: '333333333333',
+ roleName: 'hi',
+ partition: 'aws-other',
+ region: 'not-us-east-1',
+ externalId: 'there',
+ },
+ {
+ accountId: '444444444444',
+ profile: 'my-profile',
+ },
+ ],
+ accountDefaults: {
+ roleName: 'backstage-role',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'my-id',
+ },
+ mainAccount: {
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ region: 'ap-northeast-1',
+ },
+ }),
+ );
+ expect(output).toEqual({
+ accounts: [
+ {
+ accountId: '111111111111',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ roleName: 'hello',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'world',
+ },
+ {
+ accountId: '222222222222',
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ },
+ {
+ accountId: '333333333333',
+ roleName: 'hi',
+ partition: 'aws-other',
+ region: 'not-us-east-1',
+ externalId: 'there',
+ },
+ {
+ accountId: '444444444444',
+ profile: 'my-profile',
+ },
+ ],
+ accountDefaults: {
+ roleName: 'backstage-role',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'my-id',
+ },
+ mainAccount: {
+ accessKeyId: 'GHI',
+ secretAccessKey: 'JKL',
+ region: 'ap-northeast-1',
+ },
+ });
+ });
+
+ it('reads profile for main account', () => {
+ const output = readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ {
+ accountId: '111111111111',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ roleName: 'hello',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'world',
+ },
+ ],
+ accountDefaults: {
+ roleName: 'backstage-role',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'my-id',
+ },
+ mainAccount: {
+ profile: 'my-profile',
+ },
+ }),
+ );
+ expect(output).toEqual({
+ accounts: [
+ {
+ accountId: '111111111111',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ roleName: 'hello',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'world',
+ },
+ ],
+ accountDefaults: {
+ roleName: 'backstage-role',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'my-id',
+ },
+ mainAccount: {
+ profile: 'my-profile',
+ },
+ });
+ });
+
+ it('does not fail when config is not set', () => {
+ const output = readAwsIntegrationConfig(buildConfig({}));
+ expect(output).toEqual({
+ accountDefaults: {},
+ accounts: [],
+ mainAccount: {},
+ });
+ });
+
+ it('rejects invalid combinations of account attributes', () => {
+ const validAccount: any = {
+ accountId: '111111111111',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'EDF',
+ roleName: 'hello',
+ partition: 'aws',
+ region: 'us-east-1',
+ externalId: 'world',
+ };
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ accessKeyId: 'ABC',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/no secret access key/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ secretAccessKey: 'ABC',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/no access key ID/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ accessKeyId: 'ABC',
+ secretAccessKey: 'DEF',
+ profile: 'my-profile',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/only one must be specified/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ roleName: 'my-role',
+ profile: 'my-profile',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/only one must be specified/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ partition: 'aws',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/no role name/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ region: 'not-us-east-1',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/no role name/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accounts: [
+ validAccount,
+ {
+ accountId: '222222222222',
+ externalId: 'hello',
+ },
+ ],
+ }),
+ ),
+ ).toThrow(/no role name/);
+ });
+
+ it('rejects invalid combinations of main account attributes', () => {
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ mainAccount: {
+ accessKeyId: 'ABC',
+ },
+ }),
+ ),
+ ).toThrow(/no secret access key/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ mainAccount: {
+ secretAccessKey: 'ABC',
+ },
+ }),
+ ),
+ ).toThrow(/no access key ID/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ mainAccount: {
+ accessKeyId: 'ABC',
+ secretAccessKey: 'DEF',
+ profile: 'my-profile',
+ },
+ }),
+ ),
+ ).toThrow(/only one must be specified/);
+ });
+
+ it('rejects invalid combinations of account default attributes', () => {
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accountDefaults: {
+ partition: 'aws',
+ },
+ }),
+ ),
+ ).toThrow(/no role name/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accountDefaults: {
+ region: 'not-us-east-1',
+ },
+ }),
+ ),
+ ).toThrow(/no role name/);
+ expect(() =>
+ readAwsIntegrationConfig(
+ buildConfig({
+ accountDefaults: {
+ externalId: 'hello',
+ },
+ }),
+ ),
+ ).toThrow(/no role name/);
+ });
+});
diff --git a/packages/integration-aws-node/src/config.ts b/packages/integration-aws-node/src/config.ts
new file mode 100644
index 0000000000..0bc8c0ed06
--- /dev/null
+++ b/packages/integration-aws-node/src/config.ts
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config } from '@backstage/config';
+
+/**
+ * The configuration parameters for a single AWS account for the AWS integration.
+ *
+ * @public
+ */
+export type AwsIntegrationAccountConfig = {
+ /**
+ * The account ID of the target account that this matches on, e.g. "123456789012"
+ */
+ accountId: string;
+
+ /**
+ * The access key ID for a set of static AWS credentials
+ */
+ accessKeyId?: string;
+
+ /**
+ * The secret access key for a set of static AWS credentials
+ */
+ secretAccessKey?: string;
+
+ /**
+ * The configuration profile from a credentials file at ~/.aws/credentials and
+ * a configuration file at ~/.aws/config.
+ */
+ profile?: string;
+
+ /**
+ * The IAM role to assume to retrieve temporary AWS credentials
+ */
+ roleName?: string;
+
+ /**
+ * The AWS partition of the IAM role, e.g. "aws", "aws-cn"
+ */
+ partition?: string;
+
+ /**
+ * The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
+ */
+ region?: string;
+
+ /**
+ * The unique identifier needed to assume the role to retrieve temporary AWS credentials
+ */
+ externalId?: string;
+};
+
+/**
+ * The configuration parameters for the main AWS account for the AWS integration.
+ *
+ * @public
+ */
+export type AwsIntegrationMainAccountConfig = {
+ /**
+ * The access key ID for a set of static AWS credentials
+ */
+ accessKeyId?: string;
+
+ /**
+ * The secret access key for a set of static AWS credentials
+ */
+ secretAccessKey?: string;
+
+ /**
+ * The configuration profile from a credentials file at ~/.aws/credentials and
+ * a configuration file at ~/.aws/config.
+ */
+ profile?: string;
+
+ /**
+ * The STS regional endpoint to use for the main account, e.g. "ap-northeast-1"
+ */
+ region?: string;
+};
+
+/**
+ * The default configuration parameters to use for accounts for the AWS integration.
+ *
+ * @public
+ */
+export type AwsIntegrationDefaultAccountConfig = {
+ /**
+ * The IAM role to assume to retrieve temporary AWS credentials
+ */
+ roleName?: string;
+
+ /**
+ * The AWS partition of the IAM role, e.g. "aws", "aws-cn"
+ */
+ partition?: string;
+
+ /**
+ * The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
+ */
+ region?: string;
+
+ /**
+ * The unique identifier needed to assume the role to retrieve temporary AWS credentials
+ */
+ externalId?: string;
+};
+
+/**
+ * The configuration parameters for AWS account integration.
+ *
+ * @public
+ */
+export type AwsIntegrationConfig = {
+ /**
+ * Configuration for retrieving AWS accounts credentials
+ */
+ accounts: AwsIntegrationAccountConfig[];
+
+ /**
+ * Defaults for retrieving AWS account credentials
+ */
+ accountDefaults: AwsIntegrationDefaultAccountConfig;
+
+ /**
+ * Main account to use for retrieving AWS account credentials
+ */
+ mainAccount: AwsIntegrationMainAccountConfig;
+};
+
+/**
+ * Reads an AWS integration account config.
+ *
+ * @param config - The config object of a single account
+ */
+function readAwsIntegrationAccountConfig(
+ config: Config,
+): AwsIntegrationAccountConfig {
+ const accountConfig = {
+ accountId: config.getString('accountId'),
+ accessKeyId: config.getOptionalString('accessKeyId'),
+ secretAccessKey: config.getOptionalString('secretAccessKey'),
+ profile: config.getOptionalString('profile'),
+ roleName: config.getOptionalString('roleName'),
+ region: config.getOptionalString('region'),
+ partition: config.getOptionalString('partition'),
+ externalId: config.getOptionalString('externalId'),
+ };
+
+ // Validate that the account config has the right combination of attributes
+ if (accountConfig.accessKeyId && !accountConfig.secretAccessKey) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has an access key ID configured, but no secret access key.`,
+ );
+ }
+
+ if (!accountConfig.accessKeyId && accountConfig.secretAccessKey) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has a secret access key configured, but no access key ID`,
+ );
+ }
+
+ if (accountConfig.profile && accountConfig.accessKeyId) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has both an access key ID and a profile configured, but only one must be specified`,
+ );
+ }
+
+ if (accountConfig.profile && accountConfig.roleName) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has both an access key ID and a role name configured, but only one must be specified`,
+ );
+ }
+
+ if (!accountConfig.roleName && accountConfig.externalId) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has an external ID configured, but no role name.`,
+ );
+ }
+
+ if (!accountConfig.roleName && accountConfig.region) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has an STS region configured, but no role name.`,
+ );
+ }
+
+ if (!accountConfig.roleName && accountConfig.partition) {
+ throw new Error(
+ `AWS integration account ${accountConfig.accountId} has an IAM partition configured, but no role name.`,
+ );
+ }
+
+ return accountConfig;
+}
+
+/**
+ * Reads the main AWS integration account config.
+ *
+ * @param config - The config object of the main account
+ */
+function readMainAwsIntegrationAccountConfig(
+ config: Config,
+): AwsIntegrationMainAccountConfig {
+ const mainAccountConfig = {
+ accessKeyId: config.getOptionalString('accessKeyId'),
+ secretAccessKey: config.getOptionalString('secretAccessKey'),
+ profile: config.getOptionalString('profile'),
+ region: config.getOptionalString('region'),
+ };
+
+ // Validate that the account config has the right combination of attributes
+ if (mainAccountConfig.accessKeyId && !mainAccountConfig.secretAccessKey) {
+ throw new Error(
+ `The main AWS integration account has an access key ID configured, but no secret access key.`,
+ );
+ }
+
+ if (!mainAccountConfig.accessKeyId && mainAccountConfig.secretAccessKey) {
+ throw new Error(
+ `The main AWS integration account has a secret access key configured, but no access key ID`,
+ );
+ }
+
+ if (mainAccountConfig.profile && mainAccountConfig.accessKeyId) {
+ throw new Error(
+ `The main AWS integration account has both an access key ID and a profile configured, but only one must be specified`,
+ );
+ }
+
+ return mainAccountConfig;
+}
+
+/**
+ * Reads the default settings for retrieving credentials from AWS integration accounts.
+ *
+ * @param config - The config object of the default account settings
+ */
+function readAwsIntegrationAccountDefaultsConfig(
+ config: Config,
+): AwsIntegrationDefaultAccountConfig {
+ const defaultAccountConfig = {
+ roleName: config.getOptionalString('roleName'),
+ partition: config.getOptionalString('partition'),
+ region: config.getOptionalString('region'),
+ externalId: config.getOptionalString('externalId'),
+ };
+
+ // Validate that the account config has the right combination of attributes
+ if (!defaultAccountConfig.roleName && defaultAccountConfig.externalId) {
+ throw new Error(
+ `AWS integration account default configuration has an external ID configured, but no role name.`,
+ );
+ }
+
+ if (!defaultAccountConfig.roleName && defaultAccountConfig.region) {
+ throw new Error(
+ `AWS integration account default configuration has an STS region configured, but no role name.`,
+ );
+ }
+
+ if (!defaultAccountConfig.roleName && defaultAccountConfig.partition) {
+ throw new Error(
+ `AWS integration account default configuration has an IAM partition configured, but no role name.`,
+ );
+ }
+
+ return defaultAccountConfig;
+}
+
+/**
+ * Reads an AWS integration configuration
+ *
+ * @param config - the integration config object
+ * @public
+ */
+export function readAwsIntegrationConfig(config: Config): AwsIntegrationConfig {
+ const accounts = config
+ .getOptionalConfigArray('accounts')
+ ?.map(readAwsIntegrationAccountConfig);
+ const mainAccount = config.has('mainAccount')
+ ? readMainAwsIntegrationAccountConfig(config.getConfig('mainAccount'))
+ : {};
+ const accountDefaults = config.has('accountDefaults')
+ ? readAwsIntegrationAccountDefaultsConfig(
+ config.getConfig('accountDefaults'),
+ )
+ : {};
+
+ return {
+ accounts: accounts ?? [],
+ mainAccount,
+ accountDefaults,
+ };
+}
diff --git a/packages/integration-aws-node/src/index.ts b/packages/integration-aws-node/src/index.ts
new file mode 100644
index 0000000000..30f3e21281
--- /dev/null
+++ b/packages/integration-aws-node/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DefaultAwsCredentialsManager } from './DefaultAwsCredentialsManager';
+export type {
+ AwsCredentialsManager,
+ AwsCredentialProvider,
+ AwsCredentialProviderOptions,
+} from './types';
diff --git a/packages/integration-aws-node/src/types.ts b/packages/integration-aws-node/src/types.ts
new file mode 100644
index 0000000000..3ff069b20b
--- /dev/null
+++ b/packages/integration-aws-node/src/types.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
+
+/**
+ * A set of credentials information for an AWS account.
+ *
+ * @public
+ */
+export type AwsCredentialProvider = {
+ /**
+ * The AWS account ID of these credentials
+ */
+ accountId?: string;
+ /**
+ * The STS region used with these credentials
+ */
+ stsRegion?: string;
+ /**
+ * The credential identity provider to use when creating AWS SDK for Javascript V3 clients
+ */
+ sdkCredentialProvider: AwsCredentialIdentityProvider;
+};
+
+/**
+ * The options for specifying the AWS credentials to retrieve.
+ *
+ * @public
+ */
+export type AwsCredentialProviderOptions = {
+ /**
+ * The AWS account ID, e.g. '0123456789012'
+ */
+ accountId?: string;
+
+ /**
+ * The resource ARN that will be accessed with the returned credentials.
+ * If account ID or region are not specified, they will be inferred from the ARN.
+ */
+ arn?: string;
+};
+
+/**
+ * This allows implementations to be provided to retrieve AWS credentials.
+ *
+ * @public
+ */
+export interface AwsCredentialsManager {
+ /**
+ * Get credentials for an AWS account.
+ */
+ getCredentialProvider(
+ opts?: AwsCredentialProviderOptions,
+ ): Promise;
+}
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index efbce4d4e8..f359f4ea87 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,68 @@
# @backstage/integration-react
+## 1.1.8
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.2
+
+## 1.1.7
+
+### Patch Changes
+
+- 3280711113: Updated dependency `msw` to `^0.49.0`.
+- Updated dependencies
+ - @backstage/core-plugin-api@1.2.0
+ - @backstage/core-components@0.12.1
+ - @backstage/integration@1.4.1
+ - @backstage/config@1.0.5
+ - @backstage/theme@0.2.16
+
+## 1.1.7-next.4
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.1-next.4
+ - @backstage/config@1.0.5-next.1
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/integration@1.4.1-next.1
+ - @backstage/theme@0.2.16
+
+## 1.1.7-next.3
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.1-next.3
+ - @backstage/config@1.0.5-next.1
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/integration@1.4.1-next.1
+ - @backstage/theme@0.2.16
+
+## 1.1.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-plugin-api@1.2.0-next.2
+ - @backstage/core-components@0.12.1-next.2
+ - @backstage/config@1.0.5-next.1
+ - @backstage/integration@1.4.1-next.1
+ - @backstage/theme@0.2.16
+
+## 1.1.7-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.1-next.1
+ - @backstage/core-plugin-api@1.1.1-next.1
+ - @backstage/config@1.0.5-next.1
+ - @backstage/integration@1.4.1-next.1
+ - @backstage/theme@0.2.16
+
## 1.1.7-next.0
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index f448d47b03..594c0defc4 100644
--- a/packages/integration-react/package.json
+++ b/packages/integration-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
- "version": "1.1.7-next.0",
+ "version": "1.1.7",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md
index f16605e90e..a077352af9 100644
--- a/packages/integration/CHANGELOG.md
+++ b/packages/integration/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/integration
+## 1.4.1
+
+### Patch Changes
+
+- 3280711113: Updated dependency `msw` to `^0.49.0`.
+- 34b039ca9f: Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema.
+- Updated dependencies
+ - @backstage/errors@1.1.4
+ - @backstage/config@1.0.5
+
+## 1.4.1-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.5-next.1
+ - @backstage/errors@1.1.4-next.1
+
## 1.4.1-next.0
### Patch Changes
diff --git a/packages/integration/package.json b/packages/integration/package.json
index a162cd2fea..b030611b68 100644
--- a/packages/integration/package.json
+++ b/packages/integration/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration",
"description": "Helpers for managing integrations towards external systems",
- "version": "1.4.1-next.0",
+ "version": "1.4.1",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/release-manifests/CHANGELOG.md b/packages/release-manifests/CHANGELOG.md
index e8bb4dfb07..538de23e43 100644
--- a/packages/release-manifests/CHANGELOG.md
+++ b/packages/release-manifests/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/release-manifests
+## 0.0.8
+
+### Patch Changes
+
+- 3280711113: Updated dependency `msw` to `^0.49.0`.
+
## 0.0.8-next.0
### Patch Changes
diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json
index 2a53438bd2..4abeb9dc6a 100644
--- a/packages/release-manifests/package.json
+++ b/packages/release-manifests/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/release-manifests",
"description": "Helper library for receiving release manifests",
- "version": "0.0.8-next.0",
+ "version": "0.0.8",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md
index e510cae33d..4777225550 100644
--- a/packages/repo-tools/CHANGELOG.md
+++ b/packages/repo-tools/CHANGELOG.md
@@ -1,5 +1,64 @@
# @backstage/repo-tools
+## 0.1.0
+
+### Minor Changes
+
+- 99713fd671: Introducing repo-tools package
+- 03843259b4: Api reference documentation improvements
+
+ - breadcrumbs links semantics as code spans
+ - new `@config` annotation to describe related config keys
+
+### Patch Changes
+
+- 9b1193f277: declare dependencies
+- a8611bcac4: Add new command options to the `api-report`
+
+ - added `--allow-warnings`, `-a` to continue processing packages if selected packages have warnings
+ - added `--allow-all-warnings` to continue processing packages any packages have warnings
+ - added `--omit-messages`, `-o` to pass some warnings messages code to be omitted from the api-report.md files
+ - The `paths` argument for this command now takes as default the value on `workspaces.packages` inside the root package.json
+ - change the path resolution to use the `@backstage/cli-common` packages instead
+
+- 25ec5c0c3a: Include asset-types.d.ts while running the api report command
+- 71f80eb354: add the command type-deps to the repo tool package.
+- ac440299ef: Updated api docs generation to be compatible with Docusaurus 2-alpha and 2.x.
+- Updated dependencies
+ - @backstage/errors@1.1.4
+ - @backstage/cli-common@0.1.11
+
+## 0.1.0-next.2
+
+### Patch Changes
+
+- a8611bcac4: Add new command options to the `api-report`
+
+ - added `--allow-warnings`, `-a` to continue processing packages if selected packages have warnings
+ - added `--allow-all-warnings` to continue processing packages any packages have warnings
+ - added `--omit-messages`, `-o` to pass some warnings messages code to be omitted from the api-report.md files
+ - The `paths` argument for this command now takes as default the value on `workspaces.packages` inside the root package.json
+ - change the path resolution to use the `@backstage/cli-common` packages instead
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.11-next.0
+ - @backstage/errors@1.1.4-next.1
+
+## 0.1.0-next.1
+
+### Minor Changes
+
+- 03843259b4: Api reference documentation improvements
+
+ - breadcrumbs links semantics as code spans
+ - new `@config` annotation to describe related config keys
+
+### Patch Changes
+
+- 71f80eb354: add the command type-deps to the repo tool package.
+- Updated dependencies
+ - @backstage/errors@1.1.4-next.1
+
## 0.1.0-next.0
### Minor Changes
diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md
index 309ddad2a3..3378d20ea7 100644
--- a/packages/repo-tools/cli-report.md
+++ b/packages/repo-tools/cli-report.md
@@ -12,7 +12,7 @@ Options:
-h, --help
Commands:
- api-reports [options] [path...]
+ api-reports [options] [paths...]
type-deps
help [command]
```
@@ -20,12 +20,15 @@ Commands:
### `backstage-repo-tools api-reports`
```
-Usage: backstage-repo-tools api-reports [options] [path...]
+Usage: backstage-repo-tools api-reports [options] [paths...]
Options:
--ci
--tsc
--docs
+ -a, --allow-warnings
+ --allow-all-warnings
+ -o, --omit-messages
-h, --help
```
diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json
index b28e9b22f3..09d5fe03f8 100644
--- a/packages/repo-tools/package.json
+++ b/packages/repo-tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
- "version": "0.1.0-next.0",
+ "version": "0.1.0",
"publishConfig": {
"access": "public"
},
@@ -30,17 +30,38 @@
"backstage-repo-tools": "bin/backstage-repo-tools"
},
"dependencies": {
+ "@backstage/cli-common": "workspace:^",
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@microsoft/api-documenter": "^7.17.11",
"@microsoft/api-extractor": "^7.23.0",
"@microsoft/api-extractor-model": "^7.17.2",
"@microsoft/tsdoc": "0.14.1",
+ "@microsoft/tsdoc-config": "0.16.2",
"chalk": "^4.0.0",
"commander": "^9.1.0",
"fs-extra": "10.1.0",
+ "glob": "^8.0.3",
+ "is-glob": "^4.0.3",
+ "minimatch": "^5.1.1",
"ts-node": "^10.0.0"
},
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@types/is-glob": "^4.0.2",
+ "@types/mock-fs": "^4.13.0",
+ "mock-fs": "^5.1.0"
+ },
+ "peerDependencies": {
+ "@rushstack/node-core-library": "*",
+ "prettier": "^2.8.1",
+ "typescript": "> 3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "prettier": {
+ "optional": true
+ }
+ },
"files": [
"bin",
"dist/**/*.js"
diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts
index d434aae0a6..9e844d97c8 100644
--- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts
+++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts
@@ -14,9 +14,6 @@
* limitations under the License.
*/
-/* eslint-disable import/no-extraneous-dependencies */
-/* eslint-disable no-restricted-imports */
-
import {
resolve as resolvePath,
relative as relativePath,
@@ -25,7 +22,7 @@ import {
join,
} from 'path';
import { execFile } from 'child_process';
-import prettier from 'prettier';
+import type prettierType from 'prettier';
import fs from 'fs-extra';
import {
Extractor,
@@ -39,9 +36,19 @@ import {
DocNode,
IDocNodeContainerParameters,
TSDocTagSyntaxKind,
+ TSDocConfiguration,
+ Standardization,
+ DocBlockTag,
+ DocPlainText,
+ DocLinkTag,
} from '@microsoft/tsdoc';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
-import { ApiPackage, ApiModel, ApiItem } from '@microsoft/api-extractor-model';
+import {
+ ApiPackage,
+ ApiModel,
+ ApiItem,
+ ApiItemKind,
+} from '@microsoft/api-extractor-model';
import {
IMarkdownDocumenterOptions,
MarkdownDocumenter,
@@ -55,9 +62,10 @@ import {
} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter';
import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter';
import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration';
+import { paths as cliPaths } from '../../lib/paths';
+import minimatch from 'minimatch';
-const tmpDir = resolvePath(
- process.cwd(),
+const tmpDir = cliPaths.resolveTargetRoot(
'./node_modules/.cache/api-extractor',
);
@@ -203,95 +211,47 @@ ApiReportGenerator.generateReviewFileContent =
collector,
...moreArgs,
);
- return prettier.format(content, {
- ...require('@spotify/prettier-config'),
- parser: 'markdown',
- });
+
+ try {
+ const prettier = require('prettier') as typeof prettierType;
+
+ const config = prettier.resolveConfig.sync(cliPaths.targetRoot) ?? {};
+ return prettier.format(content, {
+ ...config,
+ parser: 'markdown',
+ });
+ } catch (e) {
+ // console.warn('Failed to format API report with prettier', e);
+ return content;
+ }
};
-const PACKAGE_ROOTS = ['packages', 'plugins'];
-
-const ALLOW_WARNINGS = [
- 'packages/core-components',
- 'plugins/catalog',
- 'plugins/catalog-import',
- 'plugins/git-release-manager',
- 'plugins/jenkins',
- 'plugins/kubernetes',
-];
-
-async function resolvePackagePath(
- packagePath: string,
-): Promise {
- const projectRoot = resolvePath(process.cwd());
- const fullPackageDir = resolvePath(projectRoot, packagePath);
-
- const stat = await fs.stat(fullPackageDir);
- if (!stat.isDirectory()) {
- return undefined;
- }
-
- try {
- const packageJsonPath = join(fullPackageDir, 'package.json');
- await fs.access(packageJsonPath);
- } catch (_) {
- return undefined;
- }
-
- return relativePath(projectRoot, fullPackageDir);
-}
-
-export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) {
- const packageDirs = new Array();
-
- for (const unresolvedPackageDir of unresolvedPackageDirs) {
- const packageDir = await resolvePackagePath(unresolvedPackageDir);
- if (!packageDir) {
- throw new Error(`'${unresolvedPackageDir}' is not a valid package path`);
- }
- packageDirs.push(packageDir);
- }
-
- if (packageDirs.length === 0) {
- return undefined;
- }
-
- return packageDirs;
-}
-
-export async function findPackageDirs() {
- const packageDirs = new Array();
- const projectRoot = resolvePath(process.cwd());
-
- for (const packageRoot of PACKAGE_ROOTS) {
- const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot));
- for (const dir of dirs) {
- const packageDir = await resolvePackagePath(join(packageRoot, dir));
- if (!packageDir) {
- continue;
- }
-
- packageDirs.push(packageDir);
- }
- }
-
- return packageDirs;
-}
-
export async function createTemporaryTsConfig(includedPackageDirs: string[]) {
- const path = resolvePath(process.cwd(), 'tsconfig.tmp.json');
+ const path = cliPaths.resolveTargetRoot('tsconfig.tmp.json');
process.once('exit', () => {
fs.removeSync(path);
});
+ let assetTypeFile: string[] = [];
+
+ try {
+ assetTypeFile = [
+ require.resolve('@backstage/cli/asset-types/asset-types.d.ts'),
+ ];
+ } catch {
+ /** ignore */
+ }
+
await fs.writeJson(path, {
extends: './tsconfig.json',
include: [
// These two contain global definitions that are needed for stable API report generation
- 'packages/cli/asset-types/asset-types.d.ts',
+ ...assetTypeFile,
...includedPackageDirs.map(dir => join(dir, 'src')),
],
+ // we don't exclude node_modules so that we can use the asset-types.d.ts file
+ exclude: [],
});
return path;
@@ -334,7 +294,12 @@ export async function getTsDocConfig() {
tagName: '@ignore',
syntaxKind: TSDocTagSyntaxKind.ModifierTag,
});
+ tsdocConfigFile.addTagDefinition({
+ tagName: '@config',
+ syntaxKind: TSDocTagSyntaxKind.BlockTag,
+ });
tsdocConfigFile.setSupportForTag('@ignore', true);
+ tsdocConfigFile.setSupportForTag('@config', true);
return tsdocConfigFile;
}
@@ -360,6 +325,8 @@ interface ApiExtractionOptions {
outputDir: string;
isLocalBuild: boolean;
tsconfigFilePath: string;
+ allowWarnings?: boolean | string[];
+ omitMessages?: string[];
}
export async function runApiExtraction({
@@ -367,25 +334,37 @@ export async function runApiExtraction({
outputDir,
isLocalBuild,
tsconfigFilePath,
+ allowWarnings = false,
+ omitMessages = [],
}: ApiExtractionOptions) {
await fs.remove(outputDir);
const entryPoints = packageDirs.map(packageDir => {
- return resolvePath(
- process.cwd(),
+ return cliPaths.resolveTargetRoot(
`./dist-types/${packageDir}/src/index.d.ts`,
);
});
let compilerState: CompilerState | undefined = undefined;
+ const allowWarningPkg = Array.isArray(allowWarnings) ? allowWarnings : [];
+
+ const messagesConf: { [key: string]: { logLevel: string } } = {};
+ for (const messageCode of omitMessages) {
+ messagesConf[messageCode] = {
+ logLevel: 'none',
+ };
+ }
const warnings = new Array();
for (const packageDir of packageDirs) {
console.log(`## Processing ${packageDir}`);
- const projectFolder = resolvePath(process.cwd(), packageDir);
- const packageFolder = resolvePath(
- process.cwd(),
+ const noBail = Array.isArray(allowWarnings)
+ ? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw))
+ : allowWarnings;
+
+ const projectFolder = cliPaths.resolveTargetRoot(packageDir);
+ const packageFolder = cliPaths.resolveTargetRoot(
'./dist-types',
packageDir,
);
@@ -438,6 +417,7 @@ export async function runApiExtraction({
logLevel: 'warning' as ExtractorLogLevel.Warning,
addToApiReportFile: true,
},
+ ...messagesConf,
},
tsdocMessageReporting: {
default: {
@@ -528,12 +508,15 @@ export async function runApiExtraction({
}
const warningCountAfter = await countApiReportWarnings(projectFolder);
- if (warningCountAfter > 0 && !ALLOW_WARNINGS.includes(packageDir)) {
+ if (noBail) {
+ console.log(`Skipping warnings check for ${packageDir}`);
+ }
+ if (warningCountAfter > 0 && !noBail) {
throw new Error(
`The API Report for ${packageDir} is not allowed to have warnings`,
);
}
- if (warningCountAfter === 0 && ALLOW_WARNINGS.includes(packageDir)) {
+ if (warningCountAfter === 0 && allowWarningPkg.includes(packageDir)) {
console.log(
`No need to allow warnings for ${packageDir}, it does not have any`,
);
@@ -827,6 +810,16 @@ export async function buildDocs({
}
}
+ // This class only propose is to have a different kind and be able to render links with backticks
+ class DocCodeSpanLink extends DocLinkTag {
+ static kind = 'DocCodeSpanLink';
+
+ /** @override */
+ public get kind(): string {
+ return DocCodeSpanLink.kind;
+ }
+ }
+
// This is where we actually write the markdown and where we can hook
// in the rendering of our own nodes.
class CustomCustomMarkdownEmitter extends CustomMarkdownEmitter {
@@ -845,7 +838,7 @@ export async function buildDocs({
/** @override */
protected writeNode(
docNode: DocNode,
- context: IMarkdownEmitterContext,
+ context: IMarkdownEmitterContext