From 3293fe96a43c1778d99bcf2c7e7866e78d7099e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20DOREAU?= Date: Sat, 30 May 2020 03:43:40 +0200 Subject: [PATCH 01/89] feat(core): add Tabs component --- .../core/src/components/Tabs/Tab.test.tsx | 27 ++++ packages/core/src/components/Tabs/Tab.tsx | 65 ++++++++ packages/core/src/components/Tabs/TabBar.tsx | 54 +++++++ packages/core/src/components/Tabs/TabIcon.tsx | 72 +++++++++ .../core/src/components/Tabs/TabPanel.tsx | 42 +++++ .../core/src/components/Tabs/Tabs.stories.tsx | 47 ++++++ packages/core/src/components/Tabs/Tabs.tsx | 145 ++++++++++++++++++ packages/core/src/components/Tabs/index.ts | 17 ++ packages/core/src/components/Tabs/utils.ts | 42 +++++ packages/core/src/index.ts | 1 + packages/theme/src/themes.ts | 8 + packages/theme/src/types.ts | 4 + 12 files changed, 524 insertions(+) create mode 100644 packages/core/src/components/Tabs/Tab.test.tsx create mode 100644 packages/core/src/components/Tabs/Tab.tsx create mode 100644 packages/core/src/components/Tabs/TabBar.tsx create mode 100644 packages/core/src/components/Tabs/TabIcon.tsx create mode 100644 packages/core/src/components/Tabs/TabPanel.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.stories.tsx create mode 100644 packages/core/src/components/Tabs/Tabs.tsx create mode 100644 packages/core/src/components/Tabs/index.ts create mode 100644 packages/core/src/components/Tabs/utils.ts diff --git a/packages/core/src/components/Tabs/Tab.test.tsx b/packages/core/src/components/Tabs/Tab.test.tsx new file mode 100644 index 0000000000..a841af776a --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import Tab from './Tab'; + +describe('', () => { + it('renders without exploding', () => { + const rendered = render(wrapInTestApp()); + expect(rendered.getByText('test')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/Tabs/Tab.tsx b/packages/core/src/components/Tabs/Tab.tsx new file mode 100644 index 0000000000..3726642d94 --- /dev/null +++ b/packages/core/src/components/Tabs/Tab.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Tab, withStyles, Theme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const withStylesProps = (styles: any) => (Component: any) => (props: any) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledTabProps { + label: string; + isFirstNav?: boolean; + isFirstIndex?: boolean; +} + +const tabMarginLeft = (isFirstNav: boolean, isFirstIndex: boolean) => { + if (isFirstIndex) { + if (isFirstNav) { + return '20px'; + } + return '0'; + } + return '40px'; +}; + +const tabStyles = (props: any, theme: BackstageTheme) => ({ + root: { + textTransform: 'none', + height: '64px', + fontWeight: theme.typography.fontWeightBold, + fontSize: theme.typography.pxToRem(13), + color: theme.palette.textSubtle, + marginLeft: tabMarginLeft(props.isFirstNav, props.isFirstIndex), + width: '130px', + minWidth: '130px', + '&:hover': { + outline: 'none', + backgroundColor: 'transparent', + color: theme.palette.textSubtle, + }, + }, +}); + +const StyledTab = withStylesProps(tabStyles)((props: StyledTabProps) => { + const { isFirstNav, isFirstIndex, ...rest } = props; + return ; +}); + +export default StyledTab; diff --git a/packages/core/src/components/Tabs/TabBar.tsx b/packages/core/src/components/Tabs/TabBar.tsx new file mode 100644 index 0000000000..510111dc37 --- /dev/null +++ b/packages/core/src/components/Tabs/TabBar.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { Tabs, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +interface StyledTabsProps { + value: number; + onChange: (event: React.ChangeEvent<{}>, newValue: number) => void; +} + +const useStyles = makeStyles(theme => ({ + indicator: { + display: 'flex', + justifyContent: 'center', + backgroundColor: theme.palette.tabbar.indicator, + height: '4px', + }, + flexContainer: { + alignItems: 'center', + }, + root: { + '&:last-child': { + marginLeft: 'auto', + }, + }, +})); + +const StyledTabs: FC = props => { + const classes = useStyles(props); + return ( + }} + /> + ); +}; + +export default StyledTabs; diff --git a/packages/core/src/components/Tabs/TabIcon.tsx b/packages/core/src/components/Tabs/TabIcon.tsx new file mode 100644 index 0000000000..fd58735427 --- /dev/null +++ b/packages/core/src/components/Tabs/TabIcon.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { IconButton, withStyles, Theme } from '@material-ui/core'; + +export const withStylesProps = (styles: any) => (Component: any) => ( + props: any, +) => { + const Comp = withStyles((theme: Theme) => styles(props, theme))(Component); + return ; +}; + +interface StyledIconProps { + ariaLabel: string; + children: any; + classes: any; + isNext?: boolean; + onClick: any; +} + +const iconStyles = (props: StyledIconProps) => ({ + root: { + color: '#6E6E6E', + overflow: 'visible', + fontSize: '1.5rem', + textAlign: 'center', + borderRadius: '50%', + backgroundColor: '#E6E6E6', + marginLeft: props.isNext ? 'auto' : '0', + marginRight: props.isNext ? '0' : '10px', + '&:hover': { + backgroundColor: '#E6E6E6', + opacity: '1', + }, + }, +}); + +const StyledIcon = withStylesProps(iconStyles)((props: StyledIconProps) => { + const { + classes: { root }, + ariaLabel, + onClick, + } = props; + return ( + + {props.children} + + ); +}); + +export default StyledIcon; diff --git a/packages/core/src/components/Tabs/TabPanel.tsx b/packages/core/src/components/Tabs/TabPanel.tsx new file mode 100644 index 0000000000..32536dc86d --- /dev/null +++ b/packages/core/src/components/Tabs/TabPanel.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import Box from '@material-ui/core/Box'; + +export interface TabPanelProps { + children: any; + value: any; + index: number; +} + +const TabPanel: FC = props => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +export default TabPanel; diff --git a/packages/core/src/components/Tabs/Tabs.stories.tsx b/packages/core/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 0000000000..5258e9a51a --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import Tabs from './Tabs'; + +export default { + title: 'Tabs', + component: Tabs, +}; + +const containerStyle = {}; + +export const Default = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); + +export const Expandable = () => ( +
+ ({ + label: `ANOTHER TAB`, + content:
Content {index}
, + }))} + /> +
+); diff --git a/packages/core/src/components/Tabs/Tabs.tsx b/packages/core/src/components/Tabs/Tabs.tsx new file mode 100644 index 0000000000..cb1f5d6138 --- /dev/null +++ b/packages/core/src/components/Tabs/Tabs.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, useRef, useEffect, MutableRefObject } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { AppBar } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { chunkArray, useWindowWidth } from './utils'; + +/* Import Components */ + +import TabPanel from './TabPanel'; +import TabIcon from './TabIcon'; +import Tab from './Tab'; +import TabBar from './TabBar'; + +/* Props Types */ + +interface TabProps { + label: string; + content: any; +} + +export interface TabsProps { + tabs: TabProps[]; +} + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + flexGrow: 1, + width: '100%', + }, + styledTabs: { + backgroundColor: theme.palette.tabbar.background, + }, + appbar: { + boxShadow: 'none', + backgroundColor: theme.palette.tabbar.background, + paddingLeft: '10px', + paddingRight: '10px', + }, +})); + +const Tabs: FC = ({ tabs }) => { + const classes = useStyles(); + const [value, setValue] = React.useState(0); + const [navIndex, setNavIndex] = React.useState(0); + const [chunkedTabs, setChunkedTabs] = React.useState([[]] as TabProps[][]); + const wrapper = useRef() as MutableRefObject; + + const size = useWindowWidth(); + + const handleChange = (_: React.ChangeEvent<{}>, newValue: number) => { + setValue(newValue); + }; + + const navigateToPrevChunk = () => { + setValue(navIndex - 1 === 0 ? 0 : 1); + setNavIndex(navIndex - 1); + }; + + const navigateToNextChunk = () => { + setValue(1); + setNavIndex(navIndex + 1); + }; + + const hasNextNavIndex = () => navIndex + 1 < chunkedTabs.length; + + useEffect(() => { + // Each time the window is resized we calculate how many tabs wwe can render given the window width + const padding = 20; // The AppBar padding + + const numberOfTabIcons = navIndex === 0 ? 1 : 2; + const wrapperWidth = + wrapper.current.offsetWidth - padding - numberOfTabIcons * 30; + + const numberOfChunkedElement = Math.floor(wrapperWidth / 170); + setChunkedTabs( + chunkArray([...tabs], numberOfChunkedElement) as TabProps[][], + ); + }, [size]); + + return ( +
+ +
+ + {navIndex !== 0 && ( + + + + )} + {chunkedTabs[navIndex].map((tab, index) => ( + + ))} + {hasNextNavIndex() && ( + + + + )} + +
+
+ {chunkedTabs[navIndex].map((tab, index) => ( + + {tab.content} + + ))} +
+ ); +}; + +export default Tabs; diff --git a/packages/core/src/components/Tabs/index.ts b/packages/core/src/components/Tabs/index.ts new file mode 100644 index 0000000000..03995fe2ea --- /dev/null +++ b/packages/core/src/components/Tabs/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './Tabs'; diff --git a/packages/core/src/components/Tabs/utils.ts b/packages/core/src/components/Tabs/utils.ts new file mode 100644 index 0000000000..d3fa0dbd3e --- /dev/null +++ b/packages/core/src/components/Tabs/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useEffect } from 'react'; + +export const chunkArray = (myArray: any[], chunkSize: number) => { + const results = []; + while (myArray.length) { + results.push(myArray.splice(0, chunkSize)); + } + return results; +}; + +export const useWindowWidth = () => { + const isClient = typeof window === 'object'; + const getWidth = () => (isClient ? window.innerWidth : undefined); + const [windowWidth, setWindowWidth] = useState(getWidth); + + useEffect((): any => { + if (!isClient) { + return false; + } + + const handleResize = () => setWindowWidth(getWidth()); + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + return windowWidth; +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2ceea470f4..cd53075c28 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,3 +39,4 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; +export { default as Tabs } from './components/Tabs'; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index fe3c71a212..630e81c146 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -60,6 +60,10 @@ export const lightTheme = createTheme({ icon: '#BDBDBD', background: '#404040', }, + tabbar: { + indicator: '#9BF0E1', + background: '#FFFFFF', + }, }, }); @@ -106,5 +110,9 @@ export const darkTheme = createTheme({ icon: '#181818', background: '#BDBDBD', }, + tabbar: { + indicator: '#9BF0E1', + background: '#424242', + }, }, }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c38d67d063..8d18caf4d1 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -44,6 +44,10 @@ type PaletteAdditions = { link: string; gold: string; sidebar: string; + tabbar: { + indicator: string; + background: string; + }; bursts: { fontColor: string; slackChannelText: string; From 4db2b7aed84b04bd9293201655858c0e9c5fd64b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 10:54:37 +0200 Subject: [PATCH 02/89] packages: move ConfigReader and config types to separate config package --- packages/cli/package.json | 1 + packages/cli/src/lib/app-config/index.ts | 1 - packages/cli/src/lib/app-config/loaders.ts | 2 +- packages/cli/src/lib/bundler/types.ts | 2 +- packages/config/.eslintrc.js | 6 + packages/config/README.md | 12 ++ packages/config/package.json | 38 ++++ .../types.ts => config/src/index.ts} | 9 +- .../src/reader.test.ts} | 2 +- packages/config/src/reader.ts | 176 ++++++++++++++++++ packages/config/src/types.ts | 41 ++++ packages/core-api/package.json | 1 + .../implementations/ConfigApi/ConfigReader.ts | 166 +---------------- packages/core-api/src/app/types.ts | 6 +- packages/core/package.json | 1 + packages/core/src/api-wrappers/createApp.tsx | 2 +- 16 files changed, 291 insertions(+), 175 deletions(-) create mode 100644 packages/config/.eslintrc.js create mode 100644 packages/config/README.md create mode 100644 packages/config/package.json rename packages/{cli/src/lib/app-config/types.ts => config/src/index.ts} (81%) rename packages/{core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts => config/src/reader.test.ts} (99%) create mode 100644 packages/config/src/reader.ts create mode 100644 packages/config/src/types.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 97e3aaa972..96c6261794 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { + "@backstage/config": "^0.1.1-alpha.6", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", diff --git a/packages/cli/src/lib/app-config/index.ts b/packages/cli/src/lib/app-config/index.ts index e2c80f89e1..27b9f28e14 100644 --- a/packages/cli/src/lib/app-config/index.ts +++ b/packages/cli/src/lib/app-config/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export type { AppConfig } from './types'; export { loadConfig } from './loaders'; diff --git a/packages/cli/src/lib/app-config/loaders.ts b/packages/cli/src/lib/app-config/loaders.ts index 6e6a56e6c5..e6de9216d2 100644 --- a/packages/cli/src/lib/app-config/loaders.ts +++ b/packages/cli/src/lib/app-config/loaders.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { AppConfig } from './types'; import fs from 'fs-extra'; import yaml from 'yaml'; +import { AppConfig } from '@backstage/config'; import { paths } from '../paths'; type LoadConfigOptions = { diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 1a9f49701c..9ebd668fea 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { AppConfig } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; -import { AppConfig } from '../app-config'; export type BundlingOptions = { checksEnabled: boolean; diff --git a/packages/config/.eslintrc.js b/packages/config/.eslintrc.js new file mode 100644 index 0000000000..54e1fff915 --- /dev/null +++ b/packages/config/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + 'jest/expect-expect': 0, + }, +}; diff --git a/packages/config/README.md b/packages/config/README.md new file mode 100644 index 0000000000..f532c682be --- /dev/null +++ b/packages/config/README.md @@ -0,0 +1,12 @@ +# @backstage/config + +This package provides a config API used by Backstage core, backend, and CLI. + +## Installation + +Do not install this package directly, it is an internal package used by [@backstage/core](https://www.npmjs.com/package/@backstage/core), [@backstage/cli](https://www.npmjs.com/package/@backstage/cli), and [@backstage/backend-common](https://www.npmjs.com/package/@backstage/backend-common). Depend on either of those instead. + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000000..910c38e716 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,38 @@ +{ + "name": "@backstage/config", + "description": "Config API used by Backstage core, backend, and CLI", + "version": "0.1.1-alpha.6", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/backstage", + "directory": "packages/config" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/packages/cli/src/lib/app-config/types.ts b/packages/config/src/index.ts similarity index 81% rename from packages/cli/src/lib/app-config/types.ts rename to packages/config/src/index.ts index d15cbe3787..d873bc93a9 100644 --- a/packages/cli/src/lib/app-config/types.ts +++ b/packages/config/src/index.ts @@ -14,4 +14,11 @@ * limitations under the License. */ -export type AppConfig = any; +export type { + AppConfig, + Config, + JsonArray, + JsonObject, + JsonValue, +} from './types'; +export { ConfigReader } from './reader'; diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/config/src/reader.test.ts similarity index 99% rename from packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts rename to packages/config/src/reader.test.ts index 68c1fd5353..3d3287dfe7 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts +++ b/packages/config/src/reader.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConfigReader } from './ConfigReader'; +import { ConfigReader } from './reader'; const DATA = { zero: 0, diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts new file mode 100644 index 0000000000..6596b72087 --- /dev/null +++ b/packages/config/src/reader.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig, Config, JsonValue, JsonObject } from './types'; + +const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; + +function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function typeOf(value: JsonValue | undefined): string { + if (value === null) { + return 'null'; + } else if (Array.isArray(value)) { + return 'array'; + } + const type = typeof value; + if (type === 'number' && isNaN(value as number)) { + return 'nan'; + } + return type; +} + +function typeErrorMessage(key: string, got: string, wanted: string) { + return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`; +} + +function validateString( + key: string, + value: JsonValue | undefined, +): value is string { + if (typeof value === 'string' && value.length > 0) { + return true; + } + if (value === '') { + throw new TypeError(typeErrorMessage(key, 'empty-string', 'string')); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'string')); + } + return false; +} + +export class ConfigReader implements Config { + private static readonly nullReader = new ConfigReader({}); + + static fromConfigs(configs: AppConfig[]): ConfigReader { + if (configs.length === 0) { + return new ConfigReader({}); + } + + // Merge together all configs info a single config with recursive fallback + // readers, giving the first config object in the array the highest priority. + return configs.reduceRight((previousReader, nextConfig) => { + return new ConfigReader(nextConfig, previousReader); + }, undefined!); + } + + constructor( + private readonly data: JsonObject, + private readonly fallback?: Config, + ) {} + + getConfig(key: string): Config { + const value = this.readValue(key); + const fallbackConfig = this.fallback?.getConfig(key); + if (isObject(value)) { + return new ConfigReader(value, fallbackConfig); + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); + } + return fallbackConfig ?? ConfigReader.nullReader; + } + + getConfigArray(key: string): Config[] { + const values = this.readValue(key); + if (Array.isArray(values)) { + return values.map((value, index) => { + if (isObject(value)) { + return new ConfigReader(value); + } + throw new TypeError( + typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'), + ); + }); + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'object-array'), + ); + } + return this.fallback?.getConfigArray(key) ?? []; + } + + getNumber(key: string): number | undefined { + const value = this.readValue(key); + if (typeof value === 'number' && !isNaN(value)) { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); + } + return this.fallback?.getNumber(key); + } + + getBoolean(key: string): boolean | undefined { + const value = this.readValue(key); + if (typeof value === 'boolean') { + return value; + } + if (value !== undefined) { + throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); + } + return this.fallback?.getBoolean(key); + } + + getString(key: string): string | undefined { + const value = this.readValue(key); + if (validateString(key, value)) { + return value; + } + return this.fallback?.getString(key); + } + + getStringArray(key: string): string[] | undefined { + const values = this.readValue(key); + if (Array.isArray(values)) { + for (const [index, value] of values.entries()) { + const iKey = `${key}[${index}]`; + if (!validateString(iKey, value)) { + throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string')); + } + } + return values as string[]; + } + if (values !== undefined) { + throw new TypeError( + typeErrorMessage(key, typeOf(values), 'string-array'), + ); + } + return this.fallback?.getStringArray(key); + } + + protected readValue(key: string): JsonValue | undefined { + const parts = key.split('.'); + + let value: JsonValue | undefined = this.data; + for (const part of parts) { + if (!CONFIG_KEY_PART_PATTERN.test(part)) { + throw new TypeError(`Invalid config key '${key}'`); + } + if (isObject(value)) { + value = value[part]; + } else { + value = undefined; + } + } + + return value; + } +} diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts new file mode 100644 index 0000000000..9eda6c8b32 --- /dev/null +++ b/packages/config/src/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type JsonObject = { [key in string]: JsonValue }; +export type JsonArray = JsonValue[]; +export type JsonValue = + | JsonObject + | JsonArray + | number + | string + | boolean + | null; + +export type AppConfig = JsonObject; + +export type Config = { + getConfig(key: string): Config; + + getConfigArray(key: string): Config[]; + + getNumber(key: string): number | undefined; + + getBoolean(key: string): boolean | undefined; + + getString(key: string): string | undefined; + + getStringArray(key: string): string[] | undefined; +}; diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 8139f2c54a..39a776cf2b 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -28,6 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/config": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts index f6a8bce17b..8e0637fa22 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts @@ -14,168 +14,6 @@ * limitations under the License. */ -import { ConfigApi, Config } from '../../definitions/ConfigApi'; -import { AppConfig } from '../../../app'; +import { ConfigReader as BaseConfigReader } from '@backstage/config'; -const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i; - -type JsonObject = { [key in string]: JsonValue }; -type JsonArray = JsonValue[]; -type JsonValue = JsonObject | JsonArray | number | string | boolean | null; - -function isObject(value: JsonValue | undefined): value is JsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function typeOf(value: JsonValue | undefined): string { - if (value === null) { - return 'null'; - } else if (Array.isArray(value)) { - return 'array'; - } - const type = typeof value; - if (type === 'number' && isNaN(value as number)) { - return 'nan'; - } - return type; -} - -function typeErrorMessage(key: string, got: string, wanted: string) { - return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`; -} - -function validateString( - key: string, - value: JsonValue | undefined, -): value is string { - if (typeof value === 'string' && value.length > 0) { - return true; - } - if (value === '') { - throw new TypeError(typeErrorMessage(key, 'empty-string', 'string')); - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'string')); - } - return false; -} - -export class ConfigReader implements ConfigApi { - static nullReader = new ConfigReader({}); - - static fromConfigs(configs: AppConfig[]): ConfigReader { - if (configs.length === 0) { - return new ConfigReader({}); - } - - // Merge together all configs info a single config with recursive fallback - // readers, giving the first config object in the array the highest priority. - return configs.reduceRight((previousReader, nextConfig) => { - return new ConfigReader(nextConfig, previousReader); - }, undefined); - } - - constructor( - private readonly data: JsonObject, - private readonly fallback?: ConfigApi, - ) {} - - getConfig(key: string): Config { - const value = this.readValue(key); - const fallbackConfig = this.fallback?.getConfig(key); - if (isObject(value)) { - return new ConfigReader(value, fallbackConfig); - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); - } - return fallbackConfig ?? ConfigReader.nullReader; - } - - getConfigArray(key: string): Config[] { - const values = this.readValue(key); - if (Array.isArray(values)) { - return values.map((value, index) => { - if (isObject(value)) { - return new ConfigReader(value); - } - throw new TypeError( - typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'), - ); - }); - } - if (values !== undefined) { - throw new TypeError( - typeErrorMessage(key, typeOf(values), 'object-array'), - ); - } - return this.fallback?.getConfigArray(key) ?? []; - } - - getNumber(key: string): number | undefined { - const value = this.readValue(key); - if (typeof value === 'number' && !isNaN(value)) { - return value; - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); - } - return this.fallback?.getNumber(key); - } - - getBoolean(key: string): boolean | undefined { - const value = this.readValue(key); - if (typeof value === 'boolean') { - return value; - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); - } - return this.fallback?.getBoolean(key); - } - - getString(key: string): string | undefined { - const value = this.readValue(key); - if (validateString(key, value)) { - return value; - } - return this.fallback?.getString(key); - } - - getStringArray(key: string): string[] | undefined { - const values = this.readValue(key); - if (Array.isArray(values)) { - for (const [index, value] of values.entries()) { - const iKey = `${key}[${index}]`; - if (!validateString(iKey, value)) { - throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string')); - } - } - return values as string[]; - } - if (values !== undefined) { - throw new TypeError( - typeErrorMessage(key, typeOf(values), 'string-array'), - ); - } - return this.fallback?.getStringArray(key); - } - - private readValue(key: string): JsonValue | undefined { - const parts = key.split('.'); - - let value: JsonValue | undefined = this.data; - for (const part of parts) { - if (!CONFIG_KEY_PART_PATTERN.test(part)) { - throw new TypeError(`Invalid config key '${key}'`); - } - if (isObject(value)) { - value = value[part]; - } else { - value = undefined; - } - } - - return value; - } -} +export class ConfigReader extends BaseConfigReader {} diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 953a10cbb2..4d685a6312 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -19,6 +19,7 @@ import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; import { AppTheme } from '../apis/definitions'; +import { AppConfig } from '@backstage/config'; export type BootErrorPageProps = { step: 'load-config'; @@ -31,11 +32,6 @@ export type AppComponents = { Progress: ComponentType<{}>; }; -/** - * TBD - */ -export type AppConfig = any; - /** * A function that loads in the App config that will be accessible via the ConfigApi. * diff --git a/packages/core/package.json b/packages/core/package.json index 818634238b..11fd149f0c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,6 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/config": "^0.1.1-alpha.6", "@backstage/core-api": "0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index c53a4765b7..8ed5cddad7 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -21,13 +21,13 @@ import privateExports, { defaultSystemIcons, BootErrorPageProps, AppConfigLoader, - AppConfig, } from '@backstage/core-api'; import { BrowserRouter as Router } from 'react-router-dom'; import { ErrorPage } from '../layout/ErrorPage'; import Progress from '../components/Progress'; import { lightTheme, darkTheme } from '@backstage/theme'; +import { AppConfig } from '@backstage/config'; const { PrivateAppImpl } = privateExports; From 3cc4154b9a9f4254c4a37e74a32bf3c3c723d363 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 11:43:17 +0200 Subject: [PATCH 03/89] packages/config: refactor to make it easier to extend with more reader methods --- packages/config/src/reader.ts | 150 +++++++++++++++++----------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index 6596b72087..dfdf4cd921 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -32,29 +32,12 @@ function typeOf(value: JsonValue | undefined): string { if (type === 'number' && isNaN(value as number)) { return 'nan'; } + if (type === 'string' && value === '') { + return 'empty-string'; + } return type; } -function typeErrorMessage(key: string, got: string, wanted: string) { - return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`; -} - -function validateString( - key: string, - value: JsonValue | undefined, -): value is string { - if (typeof value === 'string' && value.length > 0) { - return true; - } - if (value === '') { - throw new TypeError(typeErrorMessage(key, 'empty-string', 'string')); - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'string')); - } - return false; -} - export class ConfigReader implements Config { private static readonly nullReader = new ConfigReader({}); @@ -72,91 +55,108 @@ export class ConfigReader implements Config { constructor( private readonly data: JsonObject, - private readonly fallback?: Config, + private readonly fallback?: ConfigReader, ) {} - getConfig(key: string): Config { + getConfig(key: string): ConfigReader { const value = this.readValue(key); const fallbackConfig = this.fallback?.getConfig(key); if (isObject(value)) { return new ConfigReader(value, fallbackConfig); } if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'object')); + throw new TypeError( + `Invalid type in config for key ${key}, got ${typeOf( + value, + )}, wanted object`, + ); } return fallbackConfig ?? ConfigReader.nullReader; } - getConfigArray(key: string): Config[] { - const values = this.readValue(key); - if (Array.isArray(values)) { - return values.map((value, index) => { - if (isObject(value)) { - return new ConfigReader(value); + getConfigArray(key: string): ConfigReader[] { + const configs = this.readConfigValue(key, values => { + if (!Array.isArray(values)) { + return { expected: 'object-array' }; + } + + for (const [index, value] of values.entries()) { + if (!isObject(value)) { + return { expected: 'object-array', value, key: `${key}[${index}]` }; } - throw new TypeError( - typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'), - ); - }); - } - if (values !== undefined) { - throw new TypeError( - typeErrorMessage(key, typeOf(values), 'object-array'), - ); - } - return this.fallback?.getConfigArray(key) ?? []; + } + return true; + }); + + return (configs ?? []).map(obj => new ConfigReader(obj)); } getNumber(key: string): number | undefined { - const value = this.readValue(key); - if (typeof value === 'number' && !isNaN(value)) { - return value; - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'number')); - } - return this.fallback?.getNumber(key); + return this.readConfigValue( + key, + value => typeof value === 'number' || { expected: 'number' }, + ); } getBoolean(key: string): boolean | undefined { - const value = this.readValue(key); - if (typeof value === 'boolean') { - return value; - } - if (value !== undefined) { - throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean')); - } - return this.fallback?.getBoolean(key); + return this.readConfigValue( + key, + value => typeof value === 'boolean' || { expected: 'boolean' }, + ); } getString(key: string): string | undefined { - const value = this.readValue(key); - if (validateString(key, value)) { - return value; - } - return this.fallback?.getString(key); + return this.readConfigValue( + key, + value => + (typeof value === 'string' && value !== '') || { expected: 'string' }, + ); } getStringArray(key: string): string[] | undefined { - const values = this.readValue(key); - if (Array.isArray(values)) { + return this.readConfigValue(key, values => { + if (!Array.isArray(values)) { + return { expected: 'string-array' }; + } for (const [index, value] of values.entries()) { - const iKey = `${key}[${index}]`; - if (!validateString(iKey, value)) { - throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string')); + if (typeof value !== 'string' || value === '') { + return { expected: 'string-array', value, key: `${key}[${index}]` }; } } - return values as string[]; - } - if (values !== undefined) { - throw new TypeError( - typeErrorMessage(key, typeOf(values), 'string-array'), - ); - } - return this.fallback?.getStringArray(key); + return true; + }); } - protected readValue(key: string): JsonValue | undefined { + private readConfigValue( + key: string, + validate: ( + value: JsonValue, + ) => { expected: string; value?: JsonValue; key?: string } | true, + ): T | undefined { + const value = this.readValue(key); + + if (value === undefined) { + return this.fallback?.readConfigValue(key, validate); + } + if (value !== undefined) { + const result = validate(value); + if (result !== true) { + const { + key: keyName = key, + value: theValue = value, + expected, + } = result; + const typeName = typeOf(theValue); + throw new TypeError( + `Invalid type in config for key ${keyName}, got ${typeName}, wanted ${expected}`, + ); + } + } + + return value as T; + } + + private readValue(key: string): JsonValue | undefined { const parts = key.split('.'); let value: JsonValue | undefined = this.data; From 832ac05a90a7822866ee7cc11f7f1e96cf071f2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 12:38:46 +0200 Subject: [PATCH 04/89] packages/core-api: just re-export ConfigReader from @backstage/config --- .../implementations/ConfigApi/ConfigReader.ts | 19 ------------------- .../apis/implementations/ConfigApi/index.ts | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts deleted file mode 100644 index 8e0637fa22..0000000000 --- a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader as BaseConfigReader } from '@backstage/config'; - -export class ConfigReader extends BaseConfigReader {} diff --git a/packages/core-api/src/apis/implementations/ConfigApi/index.ts b/packages/core-api/src/apis/implementations/ConfigApi/index.ts index 8839cb948e..7c7f88a3e5 100644 --- a/packages/core-api/src/apis/implementations/ConfigApi/index.ts +++ b/packages/core-api/src/apis/implementations/ConfigApi/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { ConfigReader } from './ConfigReader'; +export { ConfigReader } from '@backstage/config'; From 70c442e8f3401d4946b5478933a29be8a7b54749 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 14:37:45 +0200 Subject: [PATCH 05/89] packages/cli: use fs.readJson --- packages/cli/src/lib/paths.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/lib/paths.ts b/packages/cli/src/lib/paths.ts index e7b351ec16..be3f3dcee8 100644 --- a/packages/cli/src/lib/paths.ts +++ b/packages/cli/src/lib/paths.ts @@ -57,8 +57,7 @@ export function findRootPath(topPath: string): string { const exists = fs.pathExistsSync(packagePath); if (exists) { try { - const contents = fs.readFileSync(packagePath, 'utf8'); - const data = JSON.parse(contents); + const data = fs.readJsonSync(packagePath); if (data.name === 'root' || data.name.includes('backstage-e2e')) { return path; } From ff9aa28301c8f31c1e83b61ac90ae39b0768c712 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 14:37:22 +0200 Subject: [PATCH 06/89] packages/cli: move config loading to separate config-loader package --- packages/cli/package.json | 1 + packages/cli/src/commands/app/build.ts | 2 +- packages/cli/src/commands/app/serve.ts | 2 +- packages/cli/src/commands/plugin/serve.ts | 2 +- packages/config-loader/.eslintrc.js | 3 + packages/config-loader/README.md | 12 ++++ packages/config-loader/package.json | 43 ++++++++++++++ .../app-config => config-loader/src}/index.ts | 3 +- .../src/loader.ts} | 17 +++--- packages/config-loader/src/paths.ts | 57 +++++++++++++++++++ packages/config-loader/src/types.ts | 20 +++++++ 11 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 packages/config-loader/.eslintrc.js create mode 100644 packages/config-loader/README.md create mode 100644 packages/config-loader/package.json rename packages/{cli/src/lib/app-config => config-loader/src}/index.ts (86%) rename packages/{cli/src/lib/app-config/loaders.ts => config-loader/src/loader.ts} (79%) create mode 100644 packages/config-loader/src/paths.ts create mode 100644 packages/config-loader/src/types.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 96c6261794..9cf60c0c4d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.1-alpha.6", + "@backstage/config-loader": "^0.1.1-alpha.6", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index fad3e6db13..d09b2abcdd 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -16,7 +16,7 @@ import { buildBundle } from '../../lib/bundler'; import { Command } from 'commander'; -import { loadConfig } from '../../lib/app-config'; +import { loadConfig } from '@backstage/config-loader'; export default async (cmd: Command) => { await buildBundle({ diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 19dfd9a7be..5758aa2bf2 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -16,7 +16,7 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; -import { loadConfig } from '../../lib/app-config'; +import { loadConfig } from '@backstage/config-loader'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 174d1fe4af..9cc6cf55d0 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -16,7 +16,7 @@ import { Command } from 'commander'; import { serveBundle } from '../../lib/bundler'; -import { loadConfig } from '../../lib/app-config'; +import { loadConfig } from '@backstage/config-loader'; export default async (cmd: Command) => { const waitForExit = await serveBundle({ diff --git a/packages/config-loader/.eslintrc.js b/packages/config-loader/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/packages/config-loader/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/packages/config-loader/README.md b/packages/config-loader/README.md new file mode 100644 index 0000000000..595241e0d5 --- /dev/null +++ b/packages/config-loader/README.md @@ -0,0 +1,12 @@ +# @backstage/config-loader + +This package provides config loading functionality used by the backend, and CLI. + +## Installation + +Do not install this package directly, it is an internal package used by [@backstage/cli](https://www.npmjs.com/package/@backstage/cli), and [@backstage/backend-common](https://www.npmjs.com/package/@backstage/backend-common). Depend on either of those instead. + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json new file mode 100644 index 0000000000..558c91f529 --- /dev/null +++ b/packages/config-loader/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/config-loader", + "description": "Config loading functionality used by Backstage backend, and CLI", + "version": "0.1.1-alpha.6", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/backstage", + "directory": "packages/config-loader" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/config": "^0.1.1-alpha.6", + "fs-extra": "^9.0.0", + "yaml": "^1.9.2" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/packages/cli/src/lib/app-config/index.ts b/packages/config-loader/src/index.ts similarity index 86% rename from packages/cli/src/lib/app-config/index.ts rename to packages/config-loader/src/index.ts index 27b9f28e14..822fb53d04 100644 --- a/packages/cli/src/lib/app-config/index.ts +++ b/packages/config-loader/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { loadConfig } from './loaders'; +export { loadConfig } from './loader'; +export type { LoadConfigOptions } from './types'; diff --git a/packages/cli/src/lib/app-config/loaders.ts b/packages/config-loader/src/loader.ts similarity index 79% rename from packages/cli/src/lib/app-config/loaders.ts rename to packages/config-loader/src/loader.ts index e6de9216d2..006d241e8a 100644 --- a/packages/cli/src/lib/app-config/loaders.ts +++ b/packages/config-loader/src/loader.ts @@ -16,20 +16,23 @@ import fs from 'fs-extra'; import yaml from 'yaml'; +import { resolve as resolvePath } from 'path'; import { AppConfig } from '@backstage/config'; -import { paths } from '../paths'; - -type LoadConfigOptions = { - // Config path, defaults to app-config.yaml in project root - configPath?: string; -}; +import { findRootPath } from './paths'; +import { LoadConfigOptions } from './types'; export async function loadConfig( options: LoadConfigOptions = {}, ): Promise { // TODO: We'll want this to be a bit more elaborate, probably adding configs for // specific env, and maybe local config for plugins. - const { configPath = paths.resolveTargetRoot('app-config.yaml') } = options; + let { configPath } = options; + if (!configPath) { + configPath = resolvePath( + findRootPath(fs.realpathSync(process.cwd())), + 'app-config.yaml', + ); + } try { const configYaml = await fs.readFile(configPath, 'utf8'); diff --git a/packages/config-loader/src/paths.ts b/packages/config-loader/src/paths.ts new file mode 100644 index 0000000000..d0e7e96e36 --- /dev/null +++ b/packages/config-loader/src/paths.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { dirname, resolve as resolvePath } from 'path'; + +/** + * Looks for a package.json that has name: "root" to identify the root of the monorepo + * + * This is a copy of the same function in the CLI + */ +export function findRootPath(topPath: string): string { + let path = topPath; + + // Some sanity check to avoid infinite loop + for (let i = 0; i < 1000; i++) { + const packagePath = resolvePath(path, 'package.json'); + const exists = fs.pathExistsSync(packagePath); + if (exists) { + try { + const data = fs.readJsonSync(packagePath); + if (data.name === 'root' || data.name.includes('backstage-e2e')) { + return path; + } + } catch (error) { + throw new Error( + `Failed to parse package.json file while searching for root, ${error}`, + ); + } + } + + const newPath = dirname(path); + if (newPath === path) { + throw new Error( + `No package.json with name "root" found as a parent of ${topPath}`, + ); + } + path = newPath; + } + + throw new Error( + `Iteration limit reached when searching for root package.json at ${topPath}`, + ); +} diff --git a/packages/config-loader/src/types.ts b/packages/config-loader/src/types.ts new file mode 100644 index 0000000000..d25a2d3e8b --- /dev/null +++ b/packages/config-loader/src/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type LoadConfigOptions = { + // Config path, defaults to app-config.yaml in project root + configPath?: string; +}; From 3bb271b12cf41892a5fed2f737de5eaf32c2477d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 16:34:14 +0200 Subject: [PATCH 07/89] packages/config,-loader: avoid circular cli dependency --- packages/config-loader/package.json | 1 - packages/config/package.json | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 558c91f529..afa1ac3ea2 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -33,7 +33,6 @@ "yaml": "^1.9.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", "@types/jest": "^25.2.2", "@types/node": "^12.0.0" }, diff --git a/packages/config/package.json b/packages/config/package.json index 910c38e716..9371988ddd 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -28,7 +28,6 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.6", "@types/jest": "^25.2.2", "@types/node": "^12.0.0" }, From d7486730578a27f314072f5711843220413cbe4f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 09:54:52 +0200 Subject: [PATCH 08/89] packages/cli: patch config packages during e2e test --- packages/cli/src/lib/tasks.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 2d592bc549..e32bb7fbce 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -111,6 +111,8 @@ export async function templatingTask( // List of local packages that we need to modify as a part of an E2E test const PATCH_PACKAGES = [ 'cli', + 'config', + 'config-loader', 'core', 'core-api', 'dev-utils', From f2c77a823926fa26b68bf46ad6729e292026ec7e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 10:37:39 +0200 Subject: [PATCH 09/89] packages/config-loader: added basic test for findRootPath --- packages/config-loader/src/paths.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/config-loader/src/paths.test.ts diff --git a/packages/config-loader/src/paths.test.ts b/packages/config-loader/src/paths.test.ts new file mode 100644 index 0000000000..dd5ed14748 --- /dev/null +++ b/packages/config-loader/src/paths.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { findRootPath } from './paths'; + +describe('findRootPath', () => { + it('should find root path', () => { + const rootPath = findRootPath(process.cwd()); + expect(typeof rootPath).toBe('string'); + }); +}); From c5425c5664c96cf4fcf97dd082f51d402594a49c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 11:41:44 +0200 Subject: [PATCH 10/89] packages/cli,config,config-loader: point straight to src and build with build --- packages/cli/src/lib/tasks.ts | 5 +++++ packages/config-loader/package.json | 5 ++--- packages/config/package.json | 5 ++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index e32bb7fbce..f16022ee97 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -194,6 +194,11 @@ export async function installWithLocalDeps(dir: string) { delete depJson['main:src']; depJson.types = 'dist/index.d.ts'; + // Ugly hack until backend packages can point straight to source + if (name === 'config' || name === 'config-loader') { + depJson.main = 'dist/index.cjs.js'; + } + await fs .writeJSON(depJsonPath, depJson, { encoding: 'utf8', spaces: 2 }) .catch(error => { diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index afa1ac3ea2..9f403af7e8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -16,11 +16,10 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli plugin:build", + "build": "backstage-cli build", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", diff --git a/packages/config/package.json b/packages/config/package.json index 9371988ddd..244fbe5799 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -16,11 +16,10 @@ "backstage" ], "license": "Apache-2.0", - "main": "dist/index.esm.js", - "main:src": "src/index.ts", + "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli plugin:build", + "build": "backstage-cli build", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", From 8a0ccc1bcd179e682ac66f85fbbe0619da685010 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 11:00:08 +0200 Subject: [PATCH 11/89] packages/cli: pass config reader to bundler --- packages/cli/src/commands/app/build.ts | 7 +++++-- packages/cli/src/commands/app/serve.ts | 7 +++++-- packages/cli/src/commands/plugin/serve.ts | 7 +++++-- packages/cli/src/lib/bundler/types.ts | 11 +++++++---- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index d09b2abcdd..4ecf54f532 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -14,14 +14,17 @@ * limitations under the License. */ -import { buildBundle } from '../../lib/bundler'; import { Command } from 'commander'; import { loadConfig } from '@backstage/config-loader'; +import { ConfigReader } from '@backstage/config'; +import { buildBundle } from '../../lib/bundler'; export default async (cmd: Command) => { + const appConfigs = await loadConfig(); await buildBundle({ entry: 'src/index', statsJsonEnabled: cmd.stats, - appConfig: await loadConfig(), + config: ConfigReader.fromConfigs(appConfigs), + appConfigs, }); }; diff --git a/packages/cli/src/commands/app/serve.ts b/packages/cli/src/commands/app/serve.ts index 5758aa2bf2..023a3a4663 100644 --- a/packages/cli/src/commands/app/serve.ts +++ b/packages/cli/src/commands/app/serve.ts @@ -15,14 +15,17 @@ */ import { Command } from 'commander'; -import { serveBundle } from '../../lib/bundler'; import { loadConfig } from '@backstage/config-loader'; +import { ConfigReader } from '@backstage/config'; +import { serveBundle } from '../../lib/bundler'; export default async (cmd: Command) => { + const appConfigs = await loadConfig(); const waitForExit = await serveBundle({ entry: 'src/index', checksEnabled: cmd.check, - appConfig: await loadConfig(), + config: ConfigReader.fromConfigs(appConfigs), + appConfigs, }); await waitForExit(); diff --git a/packages/cli/src/commands/plugin/serve.ts b/packages/cli/src/commands/plugin/serve.ts index 9cc6cf55d0..57b939fa63 100644 --- a/packages/cli/src/commands/plugin/serve.ts +++ b/packages/cli/src/commands/plugin/serve.ts @@ -15,14 +15,17 @@ */ import { Command } from 'commander'; -import { serveBundle } from '../../lib/bundler'; import { loadConfig } from '@backstage/config-loader'; +import { ConfigReader } from '@backstage/config'; +import { serveBundle } from '../../lib/bundler'; export default async (cmd: Command) => { + const appConfigs = await loadConfig(); const waitForExit = await serveBundle({ entry: 'dev/index', checksEnabled: cmd.check, - appConfig: await loadConfig(), + config: ConfigReader.fromConfigs(appConfigs), + appConfigs, }); await waitForExit(); diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 9ebd668fea..332a105182 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -14,21 +14,24 @@ * limitations under the License. */ -import { AppConfig } from '@backstage/config'; +import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; - appConfig: AppConfig[]; + config: Config; + appConfigs: AppConfig[]; }; export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; - appConfig: AppConfig[]; + config: Config; + appConfigs: AppConfig[]; }; export type BuildOptions = BundlingPathsOptions & { statsJsonEnabled: boolean; - appConfig: AppConfig[]; + config: Config; + appConfigs: AppConfig[]; }; From 8269b81a6758d2c03ab85c63e34c9fb63d72b691 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 3 Jun 2020 15:27:08 +0200 Subject: [PATCH 12/89] packages/cli: mimic upcoming publishConfig behavior in prepack --- packages/cli/src/commands/pack.ts | 15 +++++++++------ packages/cli/src/lib/diff/handlers.ts | 4 ++++ packages/cli/src/lib/tasks.ts | 11 +++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/commands/pack.ts b/packages/cli/src/commands/pack.ts index ba9c7725ae..51c7e36086 100644 --- a/packages/cli/src/commands/pack.ts +++ b/packages/cli/src/commands/pack.ts @@ -17,18 +17,21 @@ import fs from 'fs-extra'; import { paths } from '../lib/paths'; +const SKIPPED_KEYS = ['access', 'registry', 'tag']; + export const pre = async () => { const pkgPath = paths.resolveTarget('package.json'); const pkg = await fs.readJson(pkgPath); - pkg.types = 'dist/index.d.ts'; + + for (const key of Object.keys(pkg.publishConfig ?? {})) { + if (!SKIPPED_KEYS.includes(key)) { + pkg[key] = pkg.publishConfig[key]; + } + } await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); }; export const post = async () => { - const pkgPath = paths.resolveTarget('package.json'); - - const pkg = await fs.readJson(pkgPath); - pkg.types = 'src/index.ts'; - await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); + // postpack is a noop for now, since it's not called anyway }; diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index c4efacb198..40e3d4c587 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -130,6 +130,10 @@ class PackageJsonHandler { // Publish config can be removed the the target, skip in that case if (!targetPublishConf) { + if (await this.prompt('Missing publishConfig, do you want to add it?')) { + this.targetPkg.publishConfig = pkgPublishConf; + await this.write(); + } return; } diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index f16022ee97..c9b2ca94ed 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -175,7 +175,7 @@ export async function installWithLocalDeps(dir: string) { // types to dist/index.d.ts and the main:src field is removed. // Without this we get type checking errors in the e2e test if (process.env.BACKSTAGE_E2E_CLI_TEST) { - Task.section('Patchling local dependencies for e2e tests'); + Task.section('Patching local dependencies for e2e tests'); for (const name of PATCH_PACKAGES) { await Task.forItem( @@ -192,11 +192,10 @@ export async function installWithLocalDeps(dir: string) { // We want dist to be used for e2e tests delete depJson['main:src']; - depJson.types = 'dist/index.d.ts'; - - // Ugly hack until backend packages can point straight to source - if (name === 'config' || name === 'config-loader') { - depJson.main = 'dist/index.cjs.js'; + for (const key of Object.keys(depJson.publishConfig)) { + if (key !== 'access') { + depJson[key] = depJson.publishConfig[key]; + } } await fs From 0d1655c65b5a109adc9dac6eb6eddd745e6fa932 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 17:46:59 +0200 Subject: [PATCH 13/89] packages,plugins: use publishConfig to configure published entrypoints --- packages/catalog-model/package.json | 5 ++++- .../templates/default-app/plugins/welcome/package.json.hbs | 4 ++++ packages/cli/templates/default-plugin/package.json.hbs | 4 +++- packages/config-loader/package.json | 5 ++++- packages/config/package.json | 5 ++++- packages/core-api/package.json | 4 +++- packages/core/package.json | 4 +++- packages/dev-utils/package.json | 4 +++- packages/test-utils-core/package.json | 4 +++- packages/test-utils/package.json | 4 +++- packages/theme/package.json | 4 +++- plugins/catalog/package.json | 5 +++++ plugins/circleci/package.json | 5 +++++ plugins/explore/package.json | 5 +++++ plugins/graphiql/package.json | 4 +++- plugins/home-page/package.json | 5 +++++ plugins/lighthouse/package.json | 5 +++++ plugins/register-component/package.json | 5 +++++ plugins/scaffolder/package.json | 5 +++++ plugins/sentry/package.json | 5 +++++ plugins/tech-radar/package.json | 4 +++- plugins/welcome/package.json | 5 +++++ 22 files changed, 88 insertions(+), 12 deletions(-) diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 2d6df2e34e..2de2e70e8e 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -8,7 +8,10 @@ "license": "Apache-2.0", "private": true, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "scripts": { "build": "backstage-cli build", diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs index b18839bab6..0bc8fd3d2f 100644 --- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -5,6 +5,10 @@ "main:src": "src/index.ts", "types": "src/index.ts", "private": true, + "publishConfig": { + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index cd628b0406..c92795cc04 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -7,7 +7,9 @@ "license": "Apache-2.0", "private": true, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "scripts": { "build": "backstage-cli plugin:build", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 9f403af7e8..2641e988d6 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -4,7 +4,10 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/config/package.json b/packages/config/package.json index 244fbe5799..2ff5450a0b 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -4,7 +4,10 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 39a776cf2b..c91a96ab31 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/core/package.json b/packages/core/package.json index 11fd149f0c..6233f27065 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index ab595ea9a7..eea0956a8b 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 1ebe1bf072..b1ef87729d 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 136b6d6854..2a9320ee63 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/packages/theme/package.json b/packages/theme/package.json index a5d552a660..501ef6473e 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", "repository": { diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0cdc5d2618..5180406176 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index bbcac8e9eb..d58ea6b406 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "proxy": { "/circleci/api": { "target": "https://circleci.com/api/v1.1", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 36e43a96e4..400d212e69 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index a3fee6e217..d4141aa367 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -4,7 +4,9 @@ "version": "0.1.1-alpha.6", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "homepage": "https://github.com/spotify/backstage/tree/master/plugins/graphiql#readme", "repository": { diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index fa1236861c..54144cefe3 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index d8e2b53ace..fce604b609 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 053af6bb51..ac03939eef 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 7f453fde48..9b2f214afe 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 11decb4087..f539afa886 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "license": "Apache-2.0", "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 22568f8b97..5327ce7cfb 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -7,7 +7,9 @@ "license": "Apache-2.0", "private": false, "publishConfig": { - "access": "public" + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" }, "scripts": { "build": "backstage-cli plugin:build", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index b05ef1145f..6debcc9c80 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -6,6 +6,11 @@ "types": "src/index.ts", "private": true, "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", From 945362723d3263a1f21c9bb957a215fb294ccc92 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 4 Jun 2020 11:01:40 +0200 Subject: [PATCH 14/89] packages/cli,app: read config to template html and set up public path --- packages/app/public/index.html | 29 +++++++------------ packages/cli/src/lib/bundler/config.ts | 26 +++++++++++++++-- packages/cli/src/lib/bundler/paths.ts | 19 ++++++++---- packages/cli/src/lib/bundler/server.ts | 4 ++- packages/cli/src/lib/bundler/transforms.ts | 13 +-------- .../cli/templates/default-app/app-config.yaml | 1 + 6 files changed, 52 insertions(+), 40 deletions(-) diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 3d01107696..ea9208ca57 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -8,47 +8,38 @@ name="description" content="Backstage is an open platform for building developer portals" /> - + - - - + + - Backstage + <%= app.title %> - +
\ No newline at end of file diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md new file mode 100644 index 0000000000..519cfe1d85 --- /dev/null +++ b/docs/auth/oauth.md @@ -0,0 +1,151 @@ +# OAuth and OpenID Connect + +This section describes how Backstage allows plugins to request OAuth Access +Tokens and OpenID Connect ID Tokens on behalf of the user, to be used for auth +to various third party APIs. + +## Summary + +There are occasions when the user wants to perform actions towards third party +services that require authorization via OAuth. Backstage provides standardized +[Utility APIs](../getting-started/utility-apis.md) such as the +[GoogleAuthApi](../../packages/core-api/src/apis/definitions/auth.ts) for that +use-case. Backstage also includes a set of implementations of these APIs that +integrate with the [auth-backend](../../plugins/auth-backend) plugin to provide +a popup-based OAuth flow. + +## Background + +Access control in OAuth is implemented in terms of scope, which is a list of +permissions given to the app. An OAuth service can issue Access Tokens that are +tied to a certain set of scopes, such as viewing profile information, reading +and/or writing user data in the service. The scope format and handling is +specific to each OAuth provider, and the set of available scopes are typically +found in the documentation describing the auth solution of the provider, for +example +[developers.google.com/identity/protocols/oauth2/scopes](https://developers.google.com/identity/protocols/oauth2/scopes). + +As a part of logging in with an OAuth provider, the user needs to consent to +both the login itself and the set of scopes that the app is requesting to use. +This is done by loading a page provided by the OAuth provider, where a user can +choose an account to log in with, and accept or reject the request. If the user +accepts the login request, a token is issued, and any holder of the token can +use it to make authenticated requests towards the third party service. + +## OAuth in @backstage/core-api and auth-backend + +The default OAuth implementation in Backstage is based on an OAuth server-side +offline access flow, which means that it uses the backend as a helper in order +to trade credentials. A benefit of this type of flow is that it does not require +the use of third party cookies, and is robust on a wide selection of browsers +and privacy browsing plugins, strict security settings, etc. + +The implementation also uses a popup-based flow, where auth requests are handled +in a new popup window that is opened by the app. By using a popup-based flow it +is possible to request authentication at any point in the app, without requiring +a redirect. Because of this there is no need to ask for all scopes upfront, or +interrupt the app with a redirect and forcing plugin authors to take care in +restoring state after a redirect has been make. All in all it makes it much +easier to make authenticated requests inside a plugin. + +## OAuth Flow + +The following describes the OAuth flow implemented by the +[auth-backend](../../plugins/auth-backend) and +[DefaultAuthConnector](../../packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts) +in `@backstage/core-api`. + +Component and APIs can request Access or ID Tokens from any available Auth +provider. If there already exists a cached fresh token that covers (at least) +the requested scopes, it will be returned immediately. If the OAuth provider +implements token refreshes, this check will also trigger a token refresh attempt +if no session is a available. + +If new scopes are requested, or the user is not yet logged in with that +provider, a dialog is shown informing the user that they need to log in with the +specified provider. If the user agrees to continue, a separate popup window is +opened that implements the entire consent flow. + +The popup window is pointed to the `/start` endpoint of the auth provider in the +`auth-backend` plugin, which then redirects to the OAuth consent screen of the +provider. The consent screen is controlled by the OAuth provider, and will do +things like prompting the user to log in with an account, and possibly reviewing +the set of requested scopes. If the login request is accepted, the popup window +will be redirected back to the `/handler/frame` endpoint of the auth backend. +The redirect URL will contain a short-term authorization code, which is picked +up by the backend and exchanged for long-term tokens via a call to the OAuth +provider. The Access and possibly ID Token is then handed back to the main +Backstage page via `postMessage`. If the OAuth provider implements offline +refresh, a refresh token will be stored in an HTTP-only cookie scoped to the +specific provider in the `auth-backend` plugin. + +To protect against certain attacks, the above flow also includes a simple nonce +check and a lightweight CSRF protection header. The nonce check is done to +protect against attacks where an attacker tricks a user to log in with an +account of the attacker's choosing in order to gather data. In the first part of +the flow where the popup is directed to the `/start` endpoint, a nonce is +generated and placed in both a cookie and the OAuth state. The nonces received +in the cookie and OAuth state in the redirect handler are then checked, and the +auth attempt will fail if they're not valid. The CSRF protection for the +`/refresh` and `/logout` endpoints is implemented by simply checking for the +presence of a `X-Requested-With` header. + +The target origin of the `postMessage` is also of importance to keep the flow +secure. It is configured to a single value for each auth provider and +environment. Without a single configured origin, any page could open a popup and +request an access token. + +### Sequence Diagram + +The following diagram visualizes the flow described in the previous section. + + + +![](oauth-popup-flow.svg) diff --git a/docs/auth/overview.md b/docs/auth/overview.md new file mode 100644 index 0000000000..2806b2dccf --- /dev/null +++ b/docs/auth/overview.md @@ -0,0 +1,44 @@ +# User Authentication and Authorization in Backstage + +## Summary + +The purpose of the Auth APIs in Backstage are to identify the user, and to +provide a way for plugins to request access to 3rd party services on behalf of +the user (OAuth). This documentation focuses on the implementation of that +solution and how to extend it. For documentation on how to consume the Auth APIs +in a plugin, see [TODO](#TODO). + +### Accessing Third Party Services + +The main pattern for talking to third party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls we keep the coupling between the frontend and +backend low, and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to for example a session-based system, +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third party services is +through [Utility APIs](../getting-started/utility-apis.md) for each service +provider. For a full list of providers, see [TODO](#TODO). + +### Identity - TODO + +This documentation currently only covers the OAuth use-case, as identity +management is not settled yet and part of an +[upcoming milestone](https://github.com/spotify/backstage/milestone/12). + +## Further Reading + +More details are provided in dedicated sections of the documentation. + +- [OAuth](./oauth): Description of the generic OAuth flow implemented by the + [auth-backend](../../plugins/auth-backend). +- [Glossary](./glossary): Glossary of some common terms related to the auth + flows. From 2b5b3903f78a4ac4d189d181dd8a3ceb6c18b5d1 Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Tue, 9 Jun 2020 18:32:41 +0200 Subject: [PATCH 74/89] Optional namespace and name as one part of URL --- .../components/CatalogTable/CatalogTable.tsx | 7 ++- .../ComponentPage/ComponentPage.test.tsx | 2 +- .../ComponentPage/ComponentPage.tsx | 6 +- plugins/catalog/src/data/component.ts | 1 + plugins/catalog/src/data/utils.ts | 1 + plugins/catalog/src/routes.ts | 2 +- .../RegisterComponentResultDialog.tsx | 63 ++++++++++--------- 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index f04add7cc1..bea9caf273 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -31,7 +31,12 @@ const columns: TableColumn[] = [ diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx index ee5e7348b1..98fa6b4408 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -24,7 +24,7 @@ const getTestProps = (name: string) => { return { match: { params: { - name: name, + optionalNamespaceAndName: name, kind: 'Component', }, }, diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index a410be2bf4..72aef7fe16 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -40,8 +40,7 @@ const REDIRECT_DELAY = 1000; type ComponentPageProps = { match: { params: { - name: string; - namespace?: string; + optionalNamespaceAndName: string; kind: string; }; }; @@ -55,7 +54,8 @@ const ComponentPage: FC = ({ match, history }) => { const [removingPending, setRemovingPending] = useState(false); const showRemovalDialog = () => setConfirmationDialogOpen(true); const hideRemovalDialog = () => setConfirmationDialogOpen(false); - const { name, namespace, kind } = match.params; + const { optionalNamespaceAndName, kind } = match.params; + const [name, namespace] = optionalNamespaceAndName.split(':').reverse(); const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 86749c6faa..68be7588f0 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -18,6 +18,7 @@ import { ReactNode } from 'react'; export type Component = { name: string; + namespace?: string; kind: string; metadata: EntityMeta; description: ReactNode; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index b731268c41..f84bfee73f 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -24,6 +24,7 @@ import { Component } from './component'; export function entityToComponent(envelope: Entity): Component { return { name: envelope.metadata.name, + namespace: envelope.metadata.namespace, kind: envelope.kind, metadata: envelope.metadata, description: envelope.metadata.annotations?.description ?? 'placeholder', diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index f5bb319a88..69c4e651b4 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -25,6 +25,6 @@ export const rootRoute = createRouteRef({ }); export const entityRoute = createRouteRef({ icon: NoIcon, - path: '/catalog/:namespace?/:kind/:name/', + path: '/catalog/:kind/:optionalNamespaceAndName/', title: 'Entity', }); diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index 7c6e335f6e..68b06d1751 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -54,36 +54,39 @@ export const RegisterComponentResultDialog: FC = ({ The following components have been succefully created: - {entities.map((entity: any, index: number) => ( - - - - {generatePath(entityRoute.path, { - name: entity.metadata.name, - kind: entity.kind, - })} - - ), - }} - /> - - {index < entities.length - 1 && } - - ))} + {entities.map((entity: any, index: number) => { + const entityPath = generatePath(entityRoute.path, { + optionalNamespaceAndName: [ + entity.metadata.namespace, + entity.metadata.name, + ] + .filter(Boolean) + .join(':'), + kind: entity.kind, + }); + + return ( + + + + {entityPath} + + ), + }} + /> + + {index < entities.length - 1 && } + + ); + })} From a633dce818a9692bd2d9fd898bf589877e027063 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 9 Jun 2020 19:50:41 +0200 Subject: [PATCH 75/89] docs: format with prettier (#1218) --- docs/FAQ.md | 283 ++++++++++++------ .../adr002-default-catalog-file-format.md | 145 ++++----- .../adr003-avoid-default-exports.md | 47 ++- .../adr004-module-export-structure.md | 44 ++- .../adr005-catalog-core-entities.md | 51 +++- docs/architecture-terminology.md | 15 +- docs/auth/add-auth-provider.md | 71 +++-- docs/create-an-app.md | 19 +- docs/design.md | 114 +++++-- docs/getting-started/Plugin development.md | 18 +- docs/getting-started/README.md | 4 +- docs/getting-started/app-custom-theme.md | 44 ++- .../contributing-to-storybook.md | 13 +- docs/getting-started/create-a-plugin.md | 21 +- .../development-environment.md | 30 +- docs/getting-started/structure-of-a-plugin.md | 33 +- docs/getting-started/utility-apis.md | 138 +++++---- docs/publishing.md | 17 +- docs/reference/createPlugin-feature-flags.md | 7 +- docs/reference/createPlugin-router.md | 6 +- 20 files changed, 756 insertions(+), 364 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 100e3e19de..5fb131528a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -4,35 +4,67 @@ ### Can we call Backstage something different? So that it fits our company better? -Yes, Backstage is just a platform for building your own developer portal. We happen to call our internal version Backstage, as well, as a reference to our music roots. You can call your version whatever suits your team, company, or brand. +Yes, Backstage is just a platform for building your own developer portal. We +happen to call our internal version Backstage, as well, as a reference to our +music roots. You can call your version whatever suits your team, company, or +brand. ### Is Backstage a monitoring platform? -No, but it can be! Backstage is designed to be a developer portal for all your infrastructure tooling, services, and documentation. So, it's not a monitoring platform — but that doesn't mean you can't integrate a monitoring tool into Backstage by writing [a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). +No, but it can be! Backstage is designed to be a developer portal for all your +infrastructure tooling, services, and documentation. So, it's not a monitoring +platform — but that doesn't mean you can't integrate a monitoring tool into +Backstage by writing +[a plugin](https://github.com/spotify/faq#what-is-a-plugin-in-backstage). ### How is Backstage licensed? -Backstage was released as free and open software by Spotify and is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). +Backstage was released as free and open software by Spotify and is licensed +under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). ### Why did we open source Backstage? -We hope to see Backstage become the infrastructure standard everywhere. When we saw how much Backstage improved developer experience and productivity internally, we wanted to share those gains. After all, if Backstage can create order in an engineering environment as open and diverse as ours, then we're pretty sure it can create order (and boost productivity) anywhere. To learn more, read our blog post, "[What the heck is Backstage anyway?](https://backstage.io/blog/2020/03/18/what-is-backstage)" +We hope to see Backstage become the infrastructure standard everywhere. When we +saw how much Backstage improved developer experience and productivity +internally, we wanted to share those gains. After all, if Backstage can create +order in an engineering environment as open and diverse as ours, then we're +pretty sure it can create order (and boost productivity) anywhere. To learn +more, read our blog post, +"[What the heck is Backstage anyway?](https://backstage.io/blog/2020/03/18/what-is-backstage)" ### Will Spotify's internal plugins be open sourced, too? -Yes, we've already started releasing open source versions of some of the plugins we use here, and we'll continue to do so. [Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the building blocks of functionality in Backstage. We have over 120 plugins inside Spotify — many of those are specialized for our use, so will remain internal and proprietary to us. But we estimate that about a third of our existing plugins make good open source candidates. (And we'll probably end up writing some brand new ones, too.) -​ +Yes, we've already started releasing open source versions of some of the plugins +we use here, and we'll continue to do so. +[Plugins](https://github.com/spotify/faq#what-is-a-plugin-in-backstage) are the +building blocks of functionality in Backstage. We have over 120 plugins inside +Spotify — many of those are specialized for our use, so will remain internal and +proprietary to us. But we estimate that about a third of our existing plugins +make good open source candidates. (And we'll probably end up writing some brand +new ones, too.) ​ + ### What's the roadmap for Backstage? -​ -We envision three phases, which you can learn about in [our project roadmap](https://github.com/spotify/backstage#project-roadmap). Even though the open source version of Backstage is relatively new compared to our internal version, we have already begun work on various aspects of all three phases. Looking at the [milestones for active issues](https://github.com/spotify/backstage/milestones) will also give you a sense of our progress. + +​ We envision three phases, which you can learn about in +[our project roadmap](https://github.com/spotify/backstage#project-roadmap). +Even though the open source version of Backstage is relatively new compared to +our internal version, we have already begun work on various aspects of all three +phases. Looking at the +[milestones for active issues](https://github.com/spotify/backstage/milestones) +will also give you a sense of our progress. ### My company doesn't have thousands of developers or services. Is Backstage overkill? -Not at all! A core reason to adopt Backstage is to standardize how software is built at your company. It's easier to decide on those standards as a small company, and grows in importance as the company grows. Backstage sets a foundation, and an early investment in your infrastructure becomes even more valuable as you grow. +Not at all! A core reason to adopt Backstage is to standardize how software is +built at your company. It's easier to decide on those standards as a small +company, and grows in importance as the company grows. Backstage sets a +foundation, and an early investment in your infrastructure becomes even more +valuable as you grow. ### Our company has a strong design language system/brand that we want to incorporate. Does Backstage support this? -Yes! The Backstage UI is built using Material-UI. With the theming capabilities of Material-UI, you are able to adapt the interface to your brand guidelines. +Yes! The Backstage UI is built using Material-UI. With the theming capabilities +of Material-UI, you are able to adapt the interface to your brand guidelines. ## Technical FAQ: @@ -40,96 +72,155 @@ Yes! The Backstage UI is built using Material-UI. With the theming capabilities The short answer is that's what we've been using in Backstage internally. -The original decision was based on Google's Material Design being a thorough, well thought out and complete design system, with many mature and powerful libraries implemented in both the system itself and auxiliary components that we knew that we would like to use. +The original decision was based on Google's Material Design being a thorough, +well thought out and complete design system, with many mature and powerful +libraries implemented in both the system itself and auxiliary components that we +knew that we would like to use. + +It strikes a good balance between power, customizability, and ease of use. A +core focus of Backstage is to make plugin developers productive with as few +hurdles as possible. Material-UI lets plugin makers get going easily with both +well-known tech and a large flora of components. ​ -It strikes a good balance between power, customizability, and ease of use. A core focus of Backstage is to make plugin developers productive with as few hurdles as possible. Material-UI lets plugin makers get going easily with both well-known tech and a large flora of components. -​ ### What technology does Backstage use? -​ -The code base is a large-scale React application that uses TypeScript. For [Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use Node.js and GraphQL. -​ -### What is the end-to-end user flow? The happy path story. -​ -There are three main user profiles for Backstage: the integrator, the contributor, and the software engineer. -​ -The **integrator** hosts the Backstage app and configures which plugins are available to use in the app. -​ -The **contributor** adds functionality to the app by writing plugins. -​ -The **software engineer** uses the app's functionality and interacts with its plugins. -​ -### What is a "plugin" in Backstage? -​ -Plugins are what provide the feature functionality in Backstage. They are used to integrate different systems into Backstage's frontend, so that the developer gets a consistent UX, no matter what tool or service is being accessed on the other side. -​ -Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform APIs and reusable UI components. Plugins can fetch data either from the backend or an API exposed through the proxy. -​ -Learn more about [the different components](https://github.com/spotify/backstage#overview) that make up Backstage. -​ -### Do I have to write plugins in TypeScript? -​ -No, you can use JavaScript if you prefer. -​ -We want to keep the Backstage core APIs in TypeScript, but aren't forcing it on individual plugins. -​ -### How do I find out if a plugin already exists? -​ -Before you write a plugin, [search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) to see if it already exists or is in the works. If no one's thought of it yet, great! Open a new issue as [a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) and describe what your plugin will do. This will help coordinate our contributors' efforts and avoid duplicating existing functionality. -​ -In the future, we will create [a plugin gallery](https://github.com/spotify/backstage/issues/260) where people can browse and search for all available plugins. -​ -### Which plugin is used the most at Spotify? -​ -By far, our most-used plugin is our TechDocs plugin, which we use for creating technical documentation. Our philosophy at Spotify is to treat "docs like code", where you write documentation using the same workflow as you write your code. This makes it easier to create, find, and update documentation. We hope to release [the open source version](https://github.com/spotify/backstage/issues/687) in the future. (See also: "[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" above) -​ -### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? -​ -Contributors can add open source plugins to the plugins directory in [this monorepo](https://github.com/spotify/backstage). Integrators can then configure which open source plugins are available to use in their instance of the app. Open source plugins are downloaded as npm packages published in the open source repository. -​ -While we encourage using the open source model, we know there are cases where contributors might want to experiment internally or keep their plugins closed source. Contributors writing closed source plugins should develop them in the plugins directory in their own Backstage repository. Integrators also configure closed source plugins locally from the monorepo. -​ -### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? -​ -We chose GitHub because it is the tool that we are most familiar with, so that will naturally lead to integrations for GitHub being developed at an early stage. -​ -Hosting this project on GitHub does not exclude integrations with alternatives, such as GitLab or Bitbucket. We believe that in time there will be plugins that will provide functionality for these tools as well. Hopefully, contributed by the community! -​ -Also note, implementations of Backstage can be hosted wherever you feel suits your needs best. -​ -### Who maintains Backstage? -​ -Spotify will maintain the open source core, but we envision different parts of the project being maintained by various companies and contributors. We also envision a large, diverse ecosystem of open source plugins, which would be maintained by their original authors/contributors or by the community. -​ -When it comes to [deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), the system integrator (typically, the infrastructure team in your organization) maintains Backstage in your own environment. -​ -### Does Spotify provide a managed version of Backstage? -​ -No, this is not a service offering. We build the piece of software, and someone in your infrastructure team is responsible for [deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and maintaining it. -​ -### How secure is Backstage? -​ -We take security seriously. When it comes to packages and code we scan our repositories periodically and update our packages to the latest versions. When it comes to deployment of Backstage within an organisation it depends on the deployment and security setup in your organisation. Reach out to us on [Discord](https://discord.gg/MUpMjP2) if you have specific queries. -Please report sensitive security issues via Spotify's [bug-bounty program](https://hackerone.com/spotify) rather than GitHub. -​ +​ The code base is a large-scale React application that uses TypeScript. For +[Phase 2](https://github.com/spotify/backstage#project-roadmap), we plan to use +Node.js and GraphQL. ​ + +### What is the end-to-end user flow? The happy path story. + +​ There are three main user profiles for Backstage: the integrator, the +contributor, and the software engineer. ​ The **integrator** hosts the Backstage +app and configures which plugins are available to use in the app. ​ The +**contributor** adds functionality to the app by writing plugins. ​ The +**software engineer** uses the app's functionality and interacts with its +plugins. ​ + +### What is a "plugin" in Backstage? + +​ Plugins are what provide the feature functionality in Backstage. They are used +to integrate different systems into Backstage's frontend, so that the developer +gets a consistent UX, no matter what tool or service is being accessed on the +other side. ​ Each plugin is treated as a self-contained web app and can include +almost any type of content. Plugins all use a common set of platform APIs and +reusable UI components. Plugins can fetch data either from the backend or an API +exposed through the proxy. ​ Learn more about +[the different components](https://github.com/spotify/backstage#overview) that +make up Backstage. ​ + +### Do I have to write plugins in TypeScript? + +​ No, you can use JavaScript if you prefer. ​ We want to keep the Backstage core +APIs in TypeScript, but aren't forcing it on individual plugins. ​ + +### How do I find out if a plugin already exists? + +​ Before you write a plugin, +[search the plugin issues](https://github.com/spotify/backstage/issues?q=is%3Aissue+label%3Aplugin+) +to see if it already exists or is in the works. If no one's thought of it yet, +great! Open a new issue as +[a plugin suggestion](https://github.com/spotify/backstage/issues/new/choose) +and describe what your plugin will do. This will help coordinate our +contributors' efforts and avoid duplicating existing functionality. ​ In the +future, we will create +[a plugin gallery](https://github.com/spotify/backstage/issues/260) where people +can browse and search for all available plugins. ​ + +### Which plugin is used the most at Spotify? + +​ By far, our most-used plugin is our TechDocs plugin, which we use for creating +technical documentation. Our philosophy at Spotify is to treat "docs like code", +where you write documentation using the same workflow as you write your code. +This makes it easier to create, find, and update documentation. We hope to +release +[the open source version](https://github.com/spotify/backstage/issues/687) in +the future. (See also: +"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/faq#will-spotifys-internal-plugins-be-open-sourced-too)" +above) ​ + +### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? + +​ Contributors can add open source plugins to the plugins directory in +[this monorepo](https://github.com/spotify/backstage). Integrators can then +configure which open source plugins are available to use in their instance of +the app. Open source plugins are downloaded as npm packages published in the +open source repository. ​ While we encourage using the open source model, we +know there are cases where contributors might want to experiment internally or +keep their plugins closed source. Contributors writing closed source plugins +should develop them in the plugins directory in their own Backstage repository. +Integrators also configure closed source plugins locally from the monorepo. ​ + +### Any plans for integrating with other repository managers, such as GitLab or Bitbucket? + +​ We chose GitHub because it is the tool that we are most familiar with, so that +will naturally lead to integrations for GitHub being developed at an early +stage. ​ Hosting this project on GitHub does not exclude integrations with +alternatives, such as GitLab or Bitbucket. We believe that in time there will be +plugins that will provide functionality for these tools as well. Hopefully, +contributed by the community! ​ Also note, implementations of Backstage can be +hosted wherever you feel suits your needs best. ​ + +### Who maintains Backstage? + +​ Spotify will maintain the open source core, but we envision different parts of +the project being maintained by various companies and contributors. We also +envision a large, diverse ecosystem of open source plugins, which would be +maintained by their original authors/contributors or by the community. ​ When it +comes to +[deployment](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md), +the system integrator (typically, the infrastructure team in your organization) +maintains Backstage in your own environment. ​ + +### Does Spotify provide a managed version of Backstage? + +​ No, this is not a service offering. We build the piece of software, and +someone in your infrastructure team is responsible for +[deploying](https://github.com/spotify/backstage/blob/master/DEPLOYMENT.md) and +maintaining it. ​ + +### How secure is Backstage? + +​ We take security seriously. When it comes to packages and code we scan our +repositories periodically and update our packages to the latest versions. When +it comes to deployment of Backstage within an organisation it depends on the +deployment and security setup in your organisation. Reach out to us on +[Discord](https://discord.gg/MUpMjP2) if you have specific queries. + +Please report sensitive security issues via Spotify's +[bug-bounty program](https://hackerone.com/spotify) rather than GitHub. ​ + ### Does Backstage collect any information that is shared with Spotify? -​ -No. Backstage does not collect any telemetry from any third party using the platform. Spotify, and the open source community, does have access to [GitHub Insights](https://github.com/features/insights), which contains information such as contributors, commits, traffic, and dependencies. -​ -Backstage is an open platform, but you are in control of your own data. You control who has access to any data you provide to your version of Backstage and who that data is shared with. -​ + +​ No. Backstage does not collect any telemetry from any third party using the +platform. Spotify, and the open source community, does have access to +[GitHub Insights](https://github.com/features/insights), which contains +information such as contributors, commits, traffic, and dependencies. ​ +Backstage is an open platform, but you are in control of your own data. You +control who has access to any data you provide to your version of Backstage and +who that data is shared with. ​ + ### Can Backstage be used to build something other than a developer portal? -​ -Yes. The core frontend framework could be used for building any large-scale web application where (1) multiple teams are building separate parts of the app, and (2) you want the overall experience to be consistent. -​ -That being said, in [Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project we will add features that are needed for developer portals and systems for managing software ecosystems. Our ambition will be to keep Backstage modular. -​ + +​ Yes. The core frontend framework could be used for building any large-scale +web application where (1) multiple teams are building separate parts of the app, +and (2) you want the overall experience to be consistent. ​ That being said, in +[Phase 2](https://github.com/spotify/backstage#project-roadmap) of the project +we will add features that are needed for developer portals and systems for +managing software ecosystems. Our ambition will be to keep Backstage modular. ​ + ### How can I get involved? -​ -Jump right in! Come help us fix some of the [early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or write an open source plugin for Backstage, like this [Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). -​ -See all the ways you can [contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). We'd love to have you as part of the community. -​ + +​ Jump right in! Come help us fix some of the +[early bugs and first issues](https://github.com/spotify/backstage/labels/good%20first%20issue) +or reach [a new milestone](https://github.com/spotify/backstage/milestones). Or +write an open source plugin for Backstage, like this +[Lighthouse plugin](https://github.com/spotify/backstage/tree/master/plugins/lighthouse). +​ See all the ways you can +[contribute here](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md). +We'd love to have you as part of the community. ​ + ### Can I join the Backstage team? -​ -If you're interested in being part of the Backstage team, reach out to [fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) + +​ If you're interested in being part of the Backstage team, reach out to +[fossopportunities@spotify.com](mailto:fossopportunities@spotify.com) diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index 23db32cc9c..6cf649aedf 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -6,22 +6,22 @@ ## Background -Backstage comes with a software catalog functionality, that you can use to -track all your software components and more. It can be powered by data from -various sources, and one of them that is included with the package, is a -custom database backed catalog. It has the ability to keep itself updated -automatically based on the contents of little descriptor files in your -version control system of choice. Developers create these files and maintain -them side by side with their code, and the catalog system reacts accordingly. +Backstage comes with a software catalog functionality, that you can use to track +all your software components and more. It can be powered by data from various +sources, and one of them that is included with the package, is a custom database +backed catalog. It has the ability to keep itself updated automatically based on +the contents of little descriptor files in your version control system of +choice. Developers create these files and maintain them side by side with their +code, and the catalog system reacts accordingly. This ADR describes the default format of these descriptor files. ### Inspiration -Internally at Spotify, a home grown software catalog system is used heavily -and forms a core part of Backstage and other important pieces of the -infrastructure. The user experience, learnings and certain pieces of metadata -from that catalog are being carried over to the open source effort. +Internally at Spotify, a home grown software catalog system is used heavily and +forms a core part of Backstage and other important pieces of the infrastructure. +The user experience, learnings and certain pieces of metadata from that catalog +are being carried over to the open source effort. The file format described herein, also draws heavy inspiration from the [kubernetes object format](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/). @@ -35,9 +35,9 @@ inside Backstage, or by push events from a CI/CD pipelines, or by webhook triggers from the version control system, etc. Each file describes one or more entities in accordance with the -[Backstage System Model](https://github.com/spotify/backstage/issues/390). -All of these entities have a common stucture and nomenclature, and they are -stored in the software catalog from which they then can be queried. +[Backstage System Model](https://github.com/spotify/backstage/issues/390). All +of these entities have a common stucture and nomenclature, and they are stored +in the software catalog from which they then can be queried. Entities have distinct names, and they may reference each other by those names. @@ -70,9 +70,9 @@ spec: ``` The root fields `apiVersion`, `kind`, `metadata`, and `spec` are part of the -_envelope_, defining the overall structure of all kinds of entity. Likewise, -the `name`, `namespace`, `labels`, and `annotations` metadata fields are of -special significance and have reserved purposes and distinct shapes. +_envelope_, defining the overall structure of all kinds of entity. Likewise, the +`name`, `namespace`, `labels`, and `annotations` metadata fields are of special +significance and have reserved purposes and distinct shapes. See below for details about these fields. @@ -88,30 +88,31 @@ first versions of the catalog will focus on the `Component` kind. The `apiVersion`is the version of specification format for that particular entity that this file is written against. The version is used for being able to -evolve the format, and the tuple of `apiVersion` and `kind` should be enough -for a parser to know how to interpret the contents of the rest of the document. +evolve the format, and the tuple of `apiVersion` and `kind` should be enough for +a parser to know how to interpret the contents of the rest of the document. Backstage specific entities have an `apiVersion` that is prefixed with -`backstage.io/`, to distinguish them from other types of object that share -the same type of structure. This may be relevant when co-hosting these +`backstage.io/`, to distinguish them from other types of object that share the +same type of structure. This may be relevant when co-hosting these specifications with e.g. kubernetes object manifests. -Early versions of the catalog will be using beta versions, e.g. `backstage.io/v1beta1`, -to signal that the format may still change. After that, we will be using -`backstage.io/v1` and up. +Early versions of the catalog will be using beta versions, e.g. +`backstage.io/v1beta1`, to signal that the format may still change. After that, +we will be using `backstage.io/v1` and up. ### `metadata` -A structure that contains metadata about the entity, i.e. things that aren't directly -part of the entity specification itself. See below for more details about this structure. +A structure that contains metadata about the entity, i.e. things that aren't +directly part of the entity specification itself. See below for more details +about this structure. ### `spec` The actual specification data that describes the entity. -The precise structure of the `spec` depends on the `apiVersion` and `kind` combination, -and some kinds may not even have a `spec` at all. See further down in this document for -the specification structure of specific kinds. +The precise structure of the `spec` depends on the `apiVersion` and `kind` +combination, and some kinds may not even have a `spec` at all. See further down +in this document for the specification structure of specific kinds. ## Metadata @@ -119,27 +120,31 @@ The `metadata` root field has the following nested structure. ### `name` -The name of the entity. This name is both meant for human eyes to recognize the entity, -and for machines and other components to reference the entity (e.g. in URLs or from -other entity specification files). +The name of the entity. This name is both meant for human eyes to recognize the +entity, and for machines and other components to reference the entity (e.g. in +URLs or from other entity specification files). -Names must be unique per kind, within a given namespace (if specified), at any point in -time. Names may be reused at a later time, after an entity is deleted from the registry. +Names must be unique per kind, within a given namespace (if specified), at any +point in time. Names may be reused at a later time, after an entity is deleted +from the registry. -Names are required to follow a certain format. Entities that do not follow those rules -will not be accepted for registration in the catalog. The ruleset is configurable to fit -your organization's needs, but the default behavior is as follows. +Names are required to follow a certain format. Entities that do not follow those +rules will not be accepted for registration in the catalog. The ruleset is +configurable to fit your organization's needs, but the default behavior is as +follows. - Strings of length at least 1, and at most 63 -- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of `[-_.]` +- Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of + `[-_.]` Example: `visits-tracking-service`, `CircleciBuildsDs_avro_gcs` -In addition to this, names are passed through a normalization function and then compared -to the same normalized form of other entity names and made sure to not collide. This rule -of uniqueness exists to avoid situations where e.g. both `my-component` and `MyComponent` -are registered side by side, which leads to confusion and risk. The normalization function -is also configurable, but the default behavior is as follows. +In addition to this, names are passed through a normalization function and then +compared to the same normalized form of other entity names and made sure to not +collide. This rule of uniqueness exists to avoid situations where e.g. both +`my-component` and `MyComponent` are registered side by side, which leads to +confusion and risk. The normalization function is also configurable, but the +default behavior is as follows. - Strip out all characters outside of the set `[a-zA-Z0-9]` - Convert to lowercase @@ -148,55 +153,59 @@ Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs` ### `namespace` -The `name` of a namespace that the entity belongs to. This field is optional, and currently -has no special semantics apart from bounding the name uniqueness constraint if specified. -It is reserved for future use and may get broader semantic implication. +The `name` of a namespace that the entity belongs to. This field is optional, +and currently has no special semantics apart from bounding the name uniqueness +constraint if specified. It is reserved for future use and may get broader +semantic implication. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. ### `description` -A human readable description of the entity, to be shown in Backstage. Should be kept short -and informative, suitable to give an overview of the entity's purpose at a glance. More -detailed explanations and documentation should be placed elsewhere. +A human readable description of the entity, to be shown in Backstage. Should be +kept short and informative, suitable to give an overview of the entity's purpose +at a glance. More detailed explanations and documentation should be placed +elsewhere. ### `labels` -Labels are optional key/value pairs of that are attached to the entity, and their use is -identical to [kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). +Labels are optional key/value pairs of that are attached to the entity, and +their use is identical to +[kubernetes object labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/). -Their main purpose is for references to other entities, and for information that is -in one way or another classifying for the current entity. They are often used as values -in queries or filters. +Their main purpose is for references to other entities, and for information that +is in one way or another classifying for the current entity. They are often used +as values in queries or filters. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix must be a valid lowercase domain name, at most 253 characters in total. The name -part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix must be a valid lowercase domain name, at most 253 +characters in total. The name part must be sequences of `[a-zA-Z0-9]` separated +by any of `[-_.]`, at most 63 characters in total. -The `backstage.io/` prefix is reserved for use by Backstage core components. Some keys such as -`system` also have predefined semantics. +The `backstage.io/` prefix is reserved for use by Backstage core components. +Some keys such as `system` also have predefined semantics. Values are strings that follow the same restrictions as `name` above. ### `annotations` An object with arbitrary non-identifying metadata attached to the entity, -identical in use to [kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). +identical in use to +[kubernetes object annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/). -Their purpose is mainly, but not limited, to reference into external systems. This could -for example be a reference to the git ref the entity was ingested from, to monitoring -and logging systems, to pagerduty schedules, etc. +Their purpose is mainly, but not limited, to reference into external systems. +This could for example be a reference to the git ref the entity was ingested +from, to monitoring and logging systems, to pagerduty schedules, etc. Both the key and the value are strings, subject to the following restrictions. -Keys have an optional prefix followed by a slash, and then the name part which is required. -The prefix must be a valid lowercase domain name, at most 253 characters in total. The name -part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters -in total. +Keys have an optional prefix followed by a slash, and then the name part which +is required. The prefix must be a valid lowercase domain name, at most 253 +characters in total. The name part must be sequences of `[a-zA-Z0-9]` separated +by any of `[-_.]`, at most 63 characters in total. The `backstage.io/` prefix is reserved for use by Backstage core components. diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 20a402e9c2..7becebaa4b 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -6,30 +6,50 @@ ## Context -When CommonJS was the primary authoring format, the best practice was to export only one thing from a module using the `module.exports = ...` format. This aligned with the [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) of "Do one thing well". The module would be consumed (`const localName = require('the-module');`) without having to know the internal structure. +When CommonJS was the primary authoring format, the best practice was to export +only one thing from a module using the `module.exports = ...` format. This +aligned with the +[UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) of "Do one +thing well". The module would be consumed +(`const localName = require('the-module');`) without having to know the internal +structure. -Now, ESModules are the primary authoring format. They have numerous benefits, such as compile-time verification of exports, and standards-defined semantics. They have a similar mechanism known as "default exports", which allows for a consumer to `import localName from 'the-module';`. This is implicitly the same as `import { default as localName } from 'the-module';`. +Now, ESModules are the primary authoring format. They have numerous benefits, +such as compile-time verification of exports, and standards-defined semantics. +They have a similar mechanism known as "default exports", which allows for a +consumer to `import localName from 'the-module';`. This is implicitly the same +as `import { default as localName } from 'the-module';`. -However, there are numerous reasons to avoid default exports, as documented by others before: +However, there are numerous reasons to avoid default exports, as documented by +others before: - https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/ A summary: -- They add indirection by encouraging a developer to create local names for modules, increasing cognitive load and slowing down code comprehension: `import TheListThing from 'not-a-list-thing';`. -- They thwart tools, such as IDEs, that can automatically rename and refactor code. -- They promote typos and mistakes, as the imported member is completely up to the consuming developer to define. -- They are ugly in CommonJS interop, as the default property must be manually specified by the consumer. This is often hidden by Babel's module interop. -- They break re-exports due to name conflicts, forcing the developer to manually name each. +- They add indirection by encouraging a developer to create local names for + modules, increasing cognitive load and slowing down code comprehension: + `import TheListThing from 'not-a-list-thing';`. +- They thwart tools, such as IDEs, that can automatically rename and refactor + code. +- They promote typos and mistakes, as the imported member is completely up to + the consuming developer to define. +- They are ugly in CommonJS interop, as the default property must be manually + specified by the consumer. This is often hidden by Babel's module interop. +- They break re-exports due to name conflicts, forcing the developer to manually + name each. -Using named exports helps prevent needing to rename symbols, which has myriad benefts. A few are: +Using named exports helps prevent needing to rename symbols, which has myriad +benefts. A few are: - IDE tools like "Find All References" and "Go To Definition" function - Manual codebase searching ("grep", etc) is easier with a unique symbol ## Decision -We will stop using default exports except when absolutely necessary (such as [`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) modules). A workaround exists for those that would prefer to never use `default`: +We will stop using default exports except when absolutely necessary (such as +[`React.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy) modules). +A workaround exists for those that would prefer to never use `default`: ```ts const Component = React.lazy(() => @@ -39,6 +59,9 @@ const Component = React.lazy(() => ## Consequences -We will actively work to remove them from our codebases, being as explicit as possible. Have a connected component? `export const ConnectedComponent = connect(Component)`. +We will actively work to remove them from our codebases, being as explicit as +possible. Have a connected component? +`export const ConnectedComponent = connect(Component)`. -We will add tools, such as lint rules, to help migrate away from default exports. +We will add tools, such as lint rules, to help migrate away from default +exports. diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 56c675f069..54132773c9 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -6,7 +6,8 @@ ## Context -With a growing number of exports of packages like `@backstage/core`, it is becoming more and more difficult to answer questions such as +With a growing number of exports of packages like `@backstage/core`, it is +becoming more and more difficult to answer questions such as > Is the export in this module also exported by the package? @@ -14,12 +15,19 @@ or > What is exported from this directory? -We currently do not use any pattern for how to structure exports. There is a mix of package-level re-exports deep into the directory tree, shallow re-exports for each directory, exports using `*` and explicit lists of each symbol, etc. -The mix and lack of predictability makes it difficult to reason about the boundaries of a module, and for example knowing whether is is safe to export a symbol in a given file. +We currently do not use any pattern for how to structure exports. There is a mix +of package-level re-exports deep into the directory tree, shallow re-exports for +each directory, exports using `*` and explicit lists of each symbol, etc. The +mix and lack of predictability makes it difficult to reason about the boundaries +of a module, and for example knowing whether is is safe to export a symbol in a +given file. ## Decision -We will make each exported symbol traceable through index files all the way down to the root of the package, `src/index.ts`. Each index file will only re-export from its own immediate directory children, and only index files will have re-exports. This gives a file tree similar to this: +We will make each exported symbol traceable through index files all the way down +to the root of the package, `src/index.ts`. Each index file will only re-export +from its own immediate directory children, and only index files will have +re-exports. This gives a file tree similar to this: ```text index.ts @@ -33,16 +41,25 @@ lib/index.ts /helper.ts ``` -To check whether for example `SubComponentY` is exported from the package, it should be possible to traverse the index files towards the root, starting at the adjacent one. If there is any index file that doesn't export the previous one, the symbol is not publicly exported. For example, if `components/ComponentX/index.ts` exports `SubComponentY`, but `components/index.ts` does not re-export `./ComponentX`, one should be certain that `SubComponentY` is not exported outside the package. This rule would be broken if for example the root `index.ts` re-exports `./components/ComponentX` +To check whether for example `SubComponentY` is exported from the package, it +should be possible to traverse the index files towards the root, starting at the +adjacent one. If there is any index file that doesn't export the previous one, +the symbol is not publicly exported. For example, if +`components/ComponentX/index.ts` exports `SubComponentY`, but +`components/index.ts` does not re-export `./ComponentX`, one should be certain +that `SubComponentY` is not exported outside the package. This rule would be +broken if for example the root `index.ts` re-exports `./components/ComponentX` -In addition, index files that are re-exporting other index files should always use wildcard form, that is: +In addition, index files that are re-exporting other index files should always +use wildcard form, that is: ```ts // in components/index.ts export * from './ComponentX'; ``` -Index files that are re-exporting symbols from non-index files should always enumerate all exports, that is: +Index files that are re-exporting symbols from non-index files should always +enumerate all exports, that is: ```ts // in components/ComponentX/index.ts @@ -50,14 +67,16 @@ export { ComponentX } from './ComponentX'; export type { ComponentXProps } from './ComponentX'; ``` -Internal cross-directory imports are allowed from non-index modules to index modules, for example: +Internal cross-directory imports are allowed from non-index modules to index +modules, for example: ```ts // in components/ComponentX/ComponentX.tsx import { UtilityX } from '../../lib/UtilityX'; ``` -Imports that bypass an index file are discouraged, but may sometimes be necessary, for example: +Imports that bypass an index file are discouraged, but may sometimes be +necessary, for example: ```ts // in components/ComponentX/ComponentX.tsx @@ -66,6 +85,9 @@ import { helperFunc } from '../../lib/UtilityX/helper'; ## Consequences -We will actively work to rework the export structure in our codebase, prioritizing the library packages such as `@backstage/core` and `@backstage/backend-common`. +We will actively work to rework the export structure in our codebase, +prioritizing the library packages such as `@backstage/core` and +`@backstage/backend-common`. -If possible, we will add tools, such as lint rules, to help enforce the export structure. +If possible, we will add tools, such as lint rules, to help enforce the export +structure. diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index ca2a66f6b9..578eb72b91 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -6,7 +6,8 @@ ## Context -We want to standardize on a few core entities that we are tracking in the Backstage catalog. This allows us to build specific plugins around them. +We want to standardize on a few core entities that we are tracking in the +Backstage catalog. This allows us to build specific plugins around them. ## Decision @@ -14,17 +15,26 @@ Backstage should eventually support the following core entities: - **Components** are individual pieces of software - **APIs** are the boundaries between different components -- **Resources** are physical or virtual infrastructure needed to operate a component +- **Resources** are physical or virtual infrastructure needed to operate a + component ![Catalog Core Entities](catalog-core-entities.png) -For now, we'll start by only implementing support for the Component entity in the Backstage catalog. This can later be extended to APIs, Resources and other potentially useful entities. +For now, we'll start by only implementing support for the Component entity in +the Backstage catalog. This can later be extended to APIs, Resources and other +potentially useful entities. ### Component -A component is a piece of software, for example a mobile application feature, web site, backend service or data pipeline (list not exhaustive). A component can be tracked in source control, or use some existing open source or commercial software. It can implement APIs for other components to consume. In turn it might depend on APIs implemented by other components, or resources that are attached to it at runtime. +A component is a piece of software, for example a mobile application feature, +web site, backend service or data pipeline (list not exhaustive). A component +can be tracked in source control, or use some existing open source or commercial +software. It can implement APIs for other components to consume. In turn it +might depend on APIs implemented by other components, or resources that are +attached to it at runtime. -Component entities are typically defined in YAML descriptor files next to the code of the component, and could look like this (actual schema will evolve): +Component entities are typically defined in YAML descriptor files next to the +code of the component, and could look like this (actual schema will evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -37,11 +47,20 @@ spec: ### API -APIs form an abstraction that allows large software ecosystems to scale. Thus, APIs are a first class citizen in the Backstage model and the primary way to discover existing functionality in the ecosystem. +APIs form an abstraction that allows large software ecosystems to scale. Thus, +APIs are a first class citizen in the Backstage model and the primary way to +discover existing functionality in the ecosystem. -APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top. +APIs are implemented by components and make their boundaries explicit. They +might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data +schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. +framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs +exposed by components need to be in a known machine-readable format so we can +build further tooling and analysis on top. -APIs are typically indexed from existing definitions in source control and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +APIs are typically indexed from existing definitions in source control and thus +wouldn't need their own descriptor files, but would be stored in the catalog +somewhat like this (actual schema will evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -54,9 +73,11 @@ spec: service HelloService { rpc SayHello (HelloRequest) returns (HelloResponse); } + message HelloRequest { string greeting = 1; } + message HelloResponse { string reply = 1; } @@ -64,9 +85,16 @@ spec: ### Resource -Resources are the infrastructure your software needs to operate at runtime like Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and APIs will allow us to visualize and create tooling around them in Backstage. +Resources are the infrastructure your software needs to operate at runtime like +Bigtable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together +with components and APIs will allow us to visualize and create tooling around +them in Backstage. -Resources are typically indexed from declarative definitions (e.g. Terraform, GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor files, but would be stored in the catalog somewhat like this (actual schema will evolve): +Resources are typically indexed from declarative definitions (e.g. Terraform, +GCP Config Connector, AWS Cloud Formation) and/or inventories from cloud +providers (e.g. GCP Asset Inventory) and thus wouldn't need their own descriptor +files, but would be stored in the catalog somewhat like this (actual schema will +evolve): ```yaml apiVersion: backstage.io/v1beta1 @@ -80,4 +108,5 @@ spec: ## Consequences -We will continue fleshing out support for the Component entity in the Backstage catalog. +We will continue fleshing out support for the Component entity in the Backstage +catalog. diff --git a/docs/architecture-terminology.md b/docs/architecture-terminology.md index 1570607896..836b2cfa99 100644 --- a/docs/architecture-terminology.md +++ b/docs/architecture-terminology.md @@ -1,9 +1,18 @@ # Architecture and Terminology -Backstage is constructed out of three parts. We separate Backstage in this way because we see three groups of contributors that work with Backstage in three different ways. +Backstage is constructed out of three parts. We separate Backstage in this way +because we see three groups of contributors that work with Backstage in three +different ways. - Core - Base functionality built by core devs in the open source project. -- App - The app is an instance of a Backstage app that is deployed and tweaked. The app ties together core functionality with additional plugins. The app is built and maintained by app developers, usually a productivity team within a company. -- Plugins - Additional functionality to make your Backstage app useful for your company. Plugins can be specific to a company or open sourced and reusable. At Spotify we have over 100 plugins built by over 50 different teams. It has been very powerful to get contributions from various infrastructure teams added into a single unified developer experience. +- App - The app is an instance of a Backstage app that is deployed and tweaked. + The app ties together core functionality with additional plugins. The app is + built and maintained by app developers, usually a productivity team within a + company. +- Plugins - Additional functionality to make your Backstage app useful for your + company. Plugins can be specific to a company or open sourced and reusable. At + Spotify we have over 100 plugins built by over 50 different teams. It has been + very powerful to get contributions from various infrastructure teams added + into a single unified developer experience. [Back to Docs](README.md) diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 4bbbec88bb..33f1d335a0 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -2,17 +2,21 @@ ## Passport -We chose [Passport](http://www.passportjs.org/) as our authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/). +We chose [Passport](http://www.passportjs.org/) as our authentication platform +due to its comprehensive set of supported authentication +[strategies](http://www.passportjs.org/packages/). ## How to add a new strategy provider ### Quick guide -[1.](#installing-the-dependencies) Install the passport-js based provider package. +[1.](#installing-the-dependencies) Install the passport-js based provider +package. [2.](#create-implementation) Create a new folder structure for the provider. -[3.](#adding-an-oauth-based-provider) Implement the provider, extending the suitable framework if needed. +[3.](#adding-an-oauth-based-provider) Implement the provider, extending the +suitable framework if needed. [4.](#hook-it-up-to-the-backend) Add the provider to the backend. @@ -26,7 +30,8 @@ yarn add @types/passport-provider-a ### Create implementation -Make a new folder with the name of the provider following the below file structure: +Make a new folder with the name of the provider following the below file +structure: ```bash plugins/auth-backend/src/providers/providerA @@ -34,13 +39,16 @@ plugins/auth-backend/src/providers/providerA └── provider.ts ``` -**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the provider class which implements a handler for the chosen framework. +**`plugins/auth-backend/src/providers/providerA/provider.ts`** defines the +provider class which implements a handler for the chosen framework. #### Adding an OAuth based provider -If we're adding an `OAuth` based provider we would implement the [OAuthProviderHandlers](#OAuthProviderHandlers) interface. +If we're adding an `OAuth` based provider we would implement the +[OAuthProviderHandlers](#OAuthProviderHandlers) interface. -The provider class takes the provider's configuration as a class parameter. It also imports the `Strategy` from the passport package. +The provider class takes the provider's configuration as a class parameter. It +also imports the `Strategy` from the passport package. ```ts import { Strategy as ProviderAStrategy } from 'passport-provider-a'; @@ -64,9 +72,11 @@ export class ProviderAAuthProvider implements OAuthProviderHandlers { #### Adding an non-OAuth based provider -_**Note**: We have prioritized OAuth-based providers and non-OAuth providers should be considered experimental._ +_**Note**: We have prioritized OAuth-based providers and non-OAuth providers +should be considered experimental._ -An non-`OAuth` based provider could implement [AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. +An non-`OAuth` based provider could implement +[AuthProviderRouteHandlers](#AuthProviderRouteHandlers) instead. ```ts export class ProviderAAuthProvider implements AuthProviderRouteHandlers { @@ -90,9 +100,12 @@ export class ProviderAAuthProvider implements AuthProviderRouteHandlers { #### Create method -Each provider exports a create method that creates the provider instance, optionally extending a supported authorization framework. This method exists to allow for flexibility if additional frameworks are supported in the future. +Each provider exports a create method that creates the provider instance, +optionally extending a supported authorization framework. This method exists to +allow for flexibility if additional frameworks are supported in the future. -Implementing OAuth by returning an instance of `OAuthProvider` based of the provider's class: +Implementing OAuth by returning an instance of `OAuthProvider` based of the +provider's class: ```ts export function createProviderAProvider(config: AuthProviderConfig) { @@ -102,7 +115,9 @@ export function createProviderAProvider(config: AuthProviderConfig) { } ``` -Not extending with OAuth, the main difference here is that the create method is returning a instance of the class without adding the OAuth authorization framework to it. +Not extending with OAuth, the main difference here is that the create method is +returning a instance of the class without adding the OAuth authorization +framework to it. ```ts export function createProviderAProvider(config: AuthProviderConfig) { @@ -112,14 +127,22 @@ export function createProviderAProvider(config: AuthProviderConfig) { #### Verify Callback -> Strategies require what is known as a verify callback. The purpose of a verify callback is to find the user that possesses a set of credentials. -> When Passport authenticates a request, it parses the credentials contained in the request. It then invokes the verify callback with those credentials as arguments [...]. If the credentials are valid, the verify callback invokes done to supply Passport with the user that authenticated. +> Strategies require what is known as a verify callback. The purpose of a verify +> callback is to find the user that possesses a set of credentials. When +> Passport authenticates a request, it parses the credentials contained in the +> request. It then invokes the verify callback with those credentials as +> arguments [...]. If the credentials are valid, the verify callback invokes +> done to supply Passport with the user that authenticated. > -> If the credentials are not valid (for example, if the password is incorrect), done should be invoked with false instead of a user to indicate an authentication failure. +> If the credentials are not valid (for example, if the password is incorrect), +> done should be invoked with false instead of a user to indicate an +> authentication failure. > > http://www.passportjs.org/docs/configure/ -**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply re-exporting the create method to be used for hooking the provider up to the backend. +**`plugins/auth-backend/src/providers/providerA/index.ts`** is simply +re-exporting the create method to be used for hooking the provider up to the +backend. ```ts export { createProviderAProvider } from './provider'; @@ -127,7 +150,9 @@ export { createProviderAProvider } from './provider'; ### Hook it up to the backend -**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be configured properly so you need to add it to the list of configured providers, all of which implement [AuthProviderConfig](#AuthProviderConfig): +**`plugins/auth-backend/src/providers/config.ts`** The provider needs to be +configured properly so you need to add it to the list of configured providers, +all of which implement [AuthProviderConfig](#AuthProviderConfig): ```ts export const providers = [ @@ -138,7 +163,10 @@ export const providers = [ }, ``` -**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` starts it sets up routing for all the available providers by calling `createAuthProviderRouter` on each provider. You need to import the create method from the provider and add it to the factory: +**`plugins/auth-backend/src/providers/factories.ts`** When the `auth-backend` +starts it sets up routing for all the available providers by calling +`createAuthProviderRouter` on each provider. You need to import the create +method from the provider and add it to the factory: ```ts import { createProviderAProvider } from './providerA'; @@ -157,11 +185,14 @@ router.post('/auth/providerA/logout'); router.get('/auth/providerA/refresh'); // if supported ``` -As you can see each endpoint is prefixed with both `/auth` and its provider name. +As you can see each endpoint is prefixed with both `/auth` and its provider +name. ### Test the new provider -You can `curl -i localhost:7000/auth/providerA/start` and which should provide a `302` redirect with a `Location` header. Paste the url from that header into a web browser and you should be able to trigger the authorization flow. +You can `curl -i localhost:7000/auth/providerA/start` and which should provide a +`302` redirect with a `Location` header. Paste the url from that header into a +web browser and you should be able to trigger the authorization flow. --- diff --git a/docs/create-an-app.md b/docs/create-an-app.md index 8c8050c2ff..b69ff1508c 100644 --- a/docs/create-an-app.md +++ b/docs/create-an-app.md @@ -1,12 +1,16 @@ # Backstage App -To get set up quickly with your own Backstage project you can create a Backstage App. +To get set up quickly with your own Backstage project you can create a Backstage +App. -A Backstage App is a monorepo setup with `lerna` that includes everything you need to run Backstage in your own environment. +A Backstage App is a monorepo setup with `lerna` that includes everything you +need to run Backstage in your own environment. ## Create an app -To create a Backstage app, you will need to have [NodeJS](https://nodejs.org/en/download/) Active LTS Release installed (currently v12). +To create a Backstage app, you will need to have +[NodeJS](https://nodejs.org/en/download/) Active LTS Release installed +(currently v12). With `npx`: @@ -14,13 +18,15 @@ With `npx`: npx @backstage/cli create-app ``` -This will create a new Backstage App inside the current folder. The name of the app-folder is the name that was provided when prompted. +This will create a new Backstage App inside the current folder. The name of the +app-folder is the name that was provided when prompted.

create app

-Inside that directory, it will generate all the files and folder structure needed for you to run your app. +Inside that directory, it will generate all the files and folder structure +needed for you to run your app. ### Folder structure @@ -69,4 +75,5 @@ cd my-backstage-app yarn start ``` -_When `yarn start` is ready it should open up a browser window displaying your app, if not you can navigate to `http://localhost:3000`._ +_When `yarn start` is ready it should open up a browser window displaying your +app, if not you can navigate to `http://localhost:3000`._ diff --git a/docs/design.md b/docs/design.md index bcbf0bbdc5..e3816bd2e5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,70 +1,134 @@ ![header](designheader.png) -Much like Backstage Open Source, this is a *living* document! We'll keep this updated as we evolve our practices! +Much like Backstage Open Source, this is a _living_ document! We'll keep this +updated as we evolve our practices! ## 📚 Our Philosophy ### Iterative -Backstage Open Source is a newly launched endeavor, and we’re excited to scale up our design practices! With that said, we’ll be working closely with you, the community, and iterating and experimenting as we go to see what works best. As a continual work in progress, we aspire to release early and often. Not only that, we are committed to working with developers to create a seamless and easy handoff. If you’re curious to see how we grow and would like to play a role in that growth, check out the issues in this GitHub repo! +Backstage Open Source is a newly launched endeavor, and we’re excited to scale +up our design practices! With that said, we’ll be working closely with you, the +community, and iterating and experimenting as we go to see what works best. As a +continual work in progress, we aspire to release early and often. Not only that, +we are committed to working with developers to create a seamless and easy +handoff. If you’re curious to see how we grow and would like to play a role in +that growth, check out the issues in this GitHub repo! ### Collaborative -The Backstage Design Team is small but mighty, and we truly cherish the amazing opportunity we have to work with the Backstage Open Source community! Have an idea? A component request? Feel free to communicate with us via [Discord](https://discord.gg/EBHEGzX) (*#design* channel). Collaboration trumps individual speed, and we want to work with you to make Backstage work for all of our users. +The Backstage Design Team is small but mighty, and we truly cherish the amazing +opportunity we have to work with the Backstage Open Source community! Have an +idea? A component request? Feel free to communicate with us via +[Discord](https://discord.gg/EBHEGzX) (_#design_ channel). Collaboration trumps +individual speed, and we want to work with you to make Backstage work for all of +our users. ### Transparent -There are a lot of exciting things coming up and we want to keep you in the loop! Keep an eye on our Milestones in GitHub to see where we’re headed. We’ll also be posting updates in the *#design* channel on [Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you informed on the decisions we’ve made and why we’ve made them. +There are a lot of exciting things coming up and we want to keep you in the +loop! Keep an eye on our Milestones in GitHub to see where we’re headed. We’ll +also be posting updates in the _#design_ channel on +[Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you +informed on the decisions we’ve made and why we’ve made them. ## 🛠 Our Practice -The chart below details how we work. ***Stay tuned***: We are currently in the process of securing a Figma workspace for Backstage Open Source, and we plan on referencing Figma documents to share specs and prototypes with the community. + +The chart below details how we work. **_Stay tuned_**: We are currently in the +process of securing a Figma workspace for Backstage Open Source, and we plan on +referencing Figma documents to share specs and prototypes with the community. ### Creating a New Design Component -| Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 | -|:---|:---|:---|:---|:---|:---| -| Platform design team submits an issue to **spotify/Backstage GitHub** with a potential component. | Backstage community offers feedback or approval on **spotify/Backstage GitHub**. | Platform design team adjusts accordingly (as they see fit) and update the Figma DLS document. | Designed component is added to **spotify/Backstage GitHub** as an issue. | External or internal Backstage open source contributors build the component. | External or internal contributors add the component to the **Backstage Storybook**. 🎉 | +| Step 1 | Step 2 | Step 3 | Step 4 | Step 5 | Step 6 | +| :------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | +| Platform design team submits an issue to **spotify/Backstage GitHub** with a potential component. | Backstage community offers feedback or approval on **spotify/Backstage GitHub**. | Platform design team adjusts accordingly (as they see fit) and update the Figma DLS document. | Designed component is added to **spotify/Backstage GitHub** as an issue. | External or internal Backstage open source contributors build the component. | External or internal contributors add the component to the **Backstage Storybook**. 🎉 | ### Building for Backstage -| Step 1 | Step 2 | Step 3 | Step 4 | -|:---|:---|:---|:---| -| External or internal contributors use Backstage and come up with an idea of an entity to build for Backstage. |External or internal contributors refer to the Backstage Open Source design system documentation in the Figma DLS document. | External or internal contributors leverage the components and tokens from the Backstage Storybook. | External or internal contributors build their Backstage entity. | -| Step 5 | Step 6 | Step 7 | Step 8 | -|:---|:---|:---|:---| -| External or internal contributors make a pull request for their entity on spotify/Backstage GitHub for review. | Platform designers and devs review the entity and submit feedback or approval on spotify/Backstage GitHub. | External or internal contributors make the changes, pull request is approved and the entity is merged. It’s live on Backstage! 🎉 | If the entity happens to be or include a UX component, it’s added to Backstage Storybook as well. | +| Step 1 | Step 2 | Step 3 | Step 4 | +| :------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | +| External or internal contributors use Backstage and come up with an idea of an entity to build for Backstage. | External or internal contributors refer to the Backstage Open Source design system documentation in the Figma DLS document. | External or internal contributors leverage the components and tokens from the Backstage Storybook. | External or internal contributors build their Backstage entity. | +| Step 5 | Step 6 | Step 7 | Step 8 | +| :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | +| External or internal contributors make a pull request for their entity on spotify/Backstage GitHub for review. | Platform designers and devs review the entity and submit feedback or approval on spotify/Backstage GitHub. | External or internal contributors make the changes, pull request is approved and the entity is merged. It’s live on Backstage! 🎉 | If the entity happens to be or include a UX component, it’s added to Backstage Storybook as well. | -The following diagram shows the relationship between the Backstage Design System and our foundation, which comprises of [Material UI](https://material-ui.com/) that is shaped by user experience and user interface decisions made by our Backstage Design Team. Also note, we encourage you to take the core experience we’ve crafted and add custom theming to better represent your organization! +The following diagram shows the relationship between the Backstage Design System +and our foundation, which comprises of [Material UI](https://material-ui.com/) +that is shaped by user experience and user interface decisions made by our +Backstage Design Team. Also note, we encourage you to take the core experience +we’ve crafted and add custom theming to better represent your organization! ![dls](DLS.png) - ## ✅ Our Priorities + ### Backstage Design System -This is the set of building blocks for Backstage contributors to leverage as they create rad plugins for Backstage! Why reinvent the wheel when you can use components that have already been vetted by our team and the Backstage community? In the spirit of crafting a cohesive and consistent user experience across all of Backstage, we strongly urge all plugin developers to utilize our Storybook as a reference. Our design system is new and evolving, and we’ll be building it up with your help! + +This is the set of building blocks for Backstage contributors to leverage as +they create rad plugins for Backstage! Why reinvent the wheel when you can use +components that have already been vetted by our team and the Backstage +community? In the spirit of crafting a cohesive and consistent user experience +across all of Backstage, we strongly urge all plugin developers to utilize our +Storybook as a reference. Our design system is new and evolving, and we’ll be +building it up with your help! + ### Core Backstage User Experience -This is the universal user experience that is shared amongst all Backstage users. From more concrete aspects like the plugins marketplace to more abstract ones like end-to-end workflows on Backstage, we’ll be working with the community to create a core user experience that best serves you and your organization. + +This is the universal user experience that is shared amongst all Backstage +users. From more concrete aspects like the plugins marketplace to more abstract +ones like end-to-end workflows on Backstage, we’ll be working with the community +to create a core user experience that best serves you and your organization. ## ⭐️ How to Contribute -### Pick up an issue! -In the beginning, most of our issues will be centered around creating universal components for our Backstage Design System and adding them to our Storybook so plugin developers can reference them. We’ll also be creating issues that are focused on building up our core Backstage user experience. We’ll be labeling our issues in GitHub with ‘design’ and/or ‘storybook’ - so feel free to browse and tackle the tasks that interest you. If you have any questions regarding an issue, you can ask them in the comments section of the issue or on [Discord](https://discord.gg/EBHEGzX). We absolutely adore our external contributors and will send you virtual semlas for your contributions! + +### Pick up an issue! + +In the beginning, most of our issues will be centered around creating universal +components for our Backstage Design System and adding them to our Storybook so +plugin developers can reference them. We’ll also be creating issues that are +focused on building up our core Backstage user experience. We’ll be labeling our +issues in GitHub with ‘design’ and/or ‘storybook’ - so feel free to browse and +tackle the tasks that interest you. If you have any questions regarding an +issue, you can ask them in the comments section of the issue or on +[Discord](https://discord.gg/EBHEGzX). We absolutely adore our external +contributors and will send you virtual semlas for your contributions! ### Request a component. -Create an issue (label it design and assign it to katz95) or send us a message on [Discord](https://discord.gg/EBHEGzX) (*#design* channel) with details of what the component is and its relevant use cases. Your request will be reviewed by our design team and you should hear back from us within 1-2 business days. We’ll get back to you and let you know whether your requested component will get picked up by our team as something to be added to our design system. + +Create an issue (label it design and assign it to katz95) or send us a message +on [Discord](https://discord.gg/EBHEGzX) (_#design_ channel) with details of +what the component is and its relevant use cases. Your request will be reviewed +by our design team and you should hear back from us within 1-2 business days. +We’ll get back to you and let you know whether your requested component will get +picked up by our team as something to be added to our design system. ## ✏️ Resources -**[Storybook](http://storybook.backstage.io/)** - where you can view our components. If you’d like to help build up our design system, you can also add components we’ve designed to the Storybook as well. -**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be directed to the *#design* channel. +**[Storybook](http://storybook.backstage.io/)** - where you can view our +components. If you’d like to help build up our design system, you can also add +components we’ve designed to the Storybook as well. + +**[Discord](https://discord.gg/EBHEGzX)** - all design questions should be +directed to the _#design_ channel. **Documentation** + - Patterns (stay tuned) - Figma files/libraries (stay tuned) ## 🔮 Future -### Contributions from designers -Are you a designer at an organisation that’s implementing Backstage? A designer who’s fascinated by the developer productivity problem space? A designer who’s curious about open source design? We’d love for you to contribute. Behind the scenes, we’re setting up a few foundational elements to make sure that contributing to Backstage as a designer is easy. From styling guidelines to UX principles to Figma documents, we’ll make sure you’re equipped to chip in on this project. We’re excited to work with you! In the meantime, we’d love to hear from you on [Discord](https://discord.gg/EBHEGzX). +### Contributions from designers + +Are you a designer at an organisation that’s implementing Backstage? A designer +who’s fascinated by the developer productivity problem space? A designer who’s +curious about open source design? We’d love for you to contribute. Behind the +scenes, we’re setting up a few foundational elements to make sure that +contributing to Backstage as a designer is easy. From styling guidelines to UX +principles to Figma documents, we’ll make sure you’re equipped to chip in on +this project. We’re excited to work with you! In the meantime, we’d love to hear +from you on [Discord](https://discord.gg/EBHEGzX). [Back to Docs](../README.md) diff --git a/docs/getting-started/Plugin development.md b/docs/getting-started/Plugin development.md index 0f578151a2..41918b5de2 100644 --- a/docs/getting-started/Plugin development.md +++ b/docs/getting-started/Plugin development.md @@ -2,10 +2,10 @@ Backstage plugins provide features to a Backstage App. -Each plugin is treated as a self-contained web app and can include almost any type of content. -Plugins all use a common set of platform APIs and reusable UI components. -Plugins can fetch data from external sources using the regular browser APIs or by depending on -external modules to do the work. +Each plugin is treated as a self-contained web app and can include almost any +type of content. Plugins all use a common set of platform APIs and reusable UI +components. Plugins can fetch data from external sources using the regular +browser APIs or by depending on external modules to do the work.