diff --git a/README.md b/README.md index 9115c041a9..2269306118 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,23 @@ +![headline](docs/headline.png) + # [Backstage](https://backstage.io) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![](https://github.com/spotify/backstage/workflows/Frontend%20CI/badge.svg) [![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX) +![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) + ## What is Backstage? Backstage is an open platform for building developer portals. The philosophy behind Backstage is simple: Don't expose your engineers to the full complexity of your infrastructure tooling. Engineers should be shipping code — not figuring out a whole new toolset every time they want to implement the basics. Backstage allows you add "stuff" (tooling, services, features, etc.) by adding a plugin, instead of building a new tool. This saves you work and avoids the need of your team to learn how to use and support yet another tool. -![headline](headline.png) - For more information go to [backstage.io](https://backstage.io) ## What problem does Backstage solve? -As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI. +As companies grow, their infrastructure systems get messier. Backstage unifies all your infrastructure tooling, services, and documentation with a single, consistent UI. This blog post provides more examples of how Backstage is used inside Spotify: @@ -80,6 +82,7 @@ Then open http://localhost/ on your browser. ## Documentation +- [FAQs](docs/FAQ.md) - [Create a Plugin](docs/getting-started/create-a-plugin.md) - [Structure of a Plugin](docs/getting-started/structure-of-a-plugin.md) - [Frontend architecture](docs/architecture-terminology.md) diff --git a/docs/FAQ.md b/docs/FAQ.md index 84312a5202..3e0093355e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,3 +12,11 @@ It strikes a good balance between power, customisability, and ease of use. Since focus of Backstage is to make plugin developers productive with as few hurdles as possible, material-ui lets plugin makers both get going easily with well-known tech and a large flora of components. + +## Are you planning on having plugins cooked into the repo or should they be developed in separate repos? + +Additional open sourced plugins would be added to the `plugins` directory in this monorepo. + +While we encourage using the open soure model, integrators that want to experiment with +Backstage internally may also choose to develop closed source plugins in a manner that suits +them best, for example in their respective Backstage source repository. diff --git a/docs/headline.png b/docs/headline.png new file mode 100644 index 0000000000..b0f9cb8b5c Binary files /dev/null and b/docs/headline.png differ diff --git a/headline.png b/headline.png deleted file mode 100644 index 063f5be00b..0000000000 Binary files a/headline.png and /dev/null differ diff --git a/packages/app/package.json b/packages/app/package.json index ec950df39c..90ff7fa0b9 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,15 +20,14 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-router-dom": "^5.1.2", - "react-scripts": "^3.4.0", "react-use": "^13.24.0", "zen-observable": "^0.8.15" }, "scripts": { - "start": "cross-env EXTEND_ESLINT=true SKIP_PREFLIGHT_CHECK=true backstage-cli watch-deps -- react-scripts start", - "build": "cross-env EXTEND_ESLINT=true SKIP_PREFLIGHT_CHECK=true react-scripts build", - "test": "cross-env EXTEND_ESLINT=true SKIP_PREFLIGHT_CHECK=true react-scripts test", - "lint": "backstage-cli app:lint" + "start": "backstage-cli app:serve", + "build": "backstage-cli app:build", + "test": "backstage-cli test", + "lint": "backstage-cli lint" }, "browserslist": { "production": [ diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js new file mode 100644 index 0000000000..e443ceec1b --- /dev/null +++ b/packages/cli/config/jest.js @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const path = require('path'); + +// If the package has it's own jest config, we use that instead. It will have to +// manually extend @spotify/web-scripts/config/jest.config.js that is desired +if (fs.existsSync('jest.config.js')) { + module.exports = require(path.resolve('jest.config.js')); +} else if (fs.existsSync('jest.config.ts')) { + module.exports = require(path.resolve('jest.config.ts')); +} else { + const extraOptions = {}; + + // Use src/setupTests.ts as the default location for configuring test env + if (fs.existsSync('src/setupTests.ts')) { + extraOptions.setupFilesAfterEnv = ['/setupTests.ts']; + } + + module.exports = { + // We base the jest config on web-scripts, it's pretty flat so we skip any complex merging of config objects + // Config can be found here: https://github.com/spotify/web-scripts/blob/master/packages/web-scripts/config/jest.config.js + ...require('@spotify/web-scripts/config/jest.config.js'), + + ...extraOptions, + + // If the package has a jest object in package.json we merge that config in. This is the recommended + // location for configuring tests. + ...require(path.resolve('package.json')).jest, + }; +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 48aaa0dd73..9cc9ee6aa7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,8 +7,8 @@ "scripts": { "exec": "npx ts-node ./src", "build": "tsc --outDir dist --noEmit false --module CommonJS", - "lint": "web-scripts lint", - "test": "web-scripts test", + "lint": "backstage-cli lint", + "test": "backstage-cli test", "start": "nodemon ." }, "devDependencies": { @@ -42,6 +42,7 @@ "inquirer": "^7.0.4", "ora": "^4.0.3", "react-dev-utils": "^10.2.0", + "react-scripts": "^3.4.0", "recursive-readdir": "^2.2.2", "replace-in-file": "^5.0.2", "ts-loader": "^6.2.1", diff --git a/packages/cli/src/commands/plugin/lint.ts b/packages/cli/src/commands/app/build.ts similarity index 74% rename from packages/cli/src/commands/plugin/lint.ts rename to packages/cli/src/commands/app/build.ts index bacd66f1c7..5dc623831e 100644 --- a/packages/cli/src/commands/plugin/lint.ts +++ b/packages/cli/src/commands/app/build.ts @@ -14,14 +14,15 @@ * limitations under the License. */ -import { Command } from 'commander'; import { run } from '../../helpers/run'; -export default async (cmd: Command) => { - const args = ['lint', '--max-warnings=0', '--format=codeframe']; - if (cmd.fix) { - args.push('--fix'); - } +export default async () => { + const args = ['build']; - await run('web-scripts', args); + await run('react-scripts', args, { + env: { + EXTEND_ESLINT: 'true', + SKIP_PREFLIGHT_CHECK: 'true', + }, + }); }; diff --git a/plugins/home-page/jest.config.js b/packages/cli/src/commands/app/serve.ts similarity index 62% rename from plugins/home-page/jest.config.js rename to packages/cli/src/commands/app/serve.ts index 2e584f12dc..21241c99e3 100644 --- a/plugins/home-page/jest.config.js +++ b/packages/cli/src/commands/app/serve.ts @@ -14,7 +14,18 @@ * limitations under the License. */ -module.exports = { - ...require('@spotify/web-scripts/config/jest.config.js'), - setupFilesAfterEnv: ['../jest.setup.ts'], +import { run } from '../../helpers/run'; +import { watchDeps } from '../watch-deps'; + +export default async () => { + const args = ['start']; + + // Start dynamic watch and build of dependencies, then serve the app + await watchDeps(); + await run('react-scripts', args, { + env: { + EXTEND_ESLINT: 'true', + SKIP_PREFLIGHT_CHECK: 'true', + }, + }); }; diff --git a/packages/cli/src/commands/app/lint.ts b/packages/cli/src/commands/lint.ts similarity index 95% rename from packages/cli/src/commands/app/lint.ts rename to packages/cli/src/commands/lint.ts index bacd66f1c7..3fbc30f897 100644 --- a/packages/cli/src/commands/app/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -15,7 +15,7 @@ */ import { Command } from 'commander'; -import { run } from '../../helpers/run'; +import { run } from '../helpers/run'; export default async (cmd: Command) => { const args = ['lint', '--max-warnings=0', '--format=codeframe']; diff --git a/packages/core/jest.config.js b/packages/cli/src/commands/testCommand.ts similarity index 58% rename from packages/core/jest.config.js rename to packages/cli/src/commands/testCommand.ts index 2e584f12dc..f632670184 100644 --- a/packages/core/jest.config.js +++ b/packages/cli/src/commands/testCommand.ts @@ -14,7 +14,23 @@ * limitations under the License. */ -module.exports = { - ...require('@spotify/web-scripts/config/jest.config.js'), - setupFilesAfterEnv: ['../jest.setup.ts'], +import { Command } from 'commander'; +import { resolve as resolvePath } from 'path'; +import { run } from '../helpers/run'; + +export default async (cmd: Command) => { + const args = [ + 'test', + '--config', + resolvePath(__dirname, '../../config/jest.js'), + ]; + + if (cmd.watch) { + args.push('--watch'); + } + if (cmd.coverage) { + args.push('--coverage'); + } + + await run('web-scripts', args, { stdio: 'inherit' }); }; diff --git a/packages/cli/src/commands/watch-deps/index.ts b/packages/cli/src/commands/watch-deps/index.ts index 6ea1fae23b..e5993ae9d6 100644 --- a/packages/cli/src/commands/watch-deps/index.ts +++ b/packages/cli/src/commands/watch-deps/index.ts @@ -31,16 +31,9 @@ const PACKAGE_BLACKLIST = [ const WATCH_LOCATIONS = ['package.json', 'src', 'assets']; -/* - * The watch-deps command is meant to improve iteration speed while working in a large monorepo - * with packages that are built independently, meaning packages depends on each other's build output. - * - * The command traverses all dependencies of the current package within the monorepo, and starts - * watching for updates in all those packages. If a change is detected, we stop listening for changes, - * and instead start up watch mode for that package. Starting watch mode means running the first - * available yarn script out of "build:watch", "watch", or "build" --watch. - */ -export default async (_command: any, args: string[]) => { +// Start watching for dependency changes. +// The returned promise resolves when watchers have started for all current dependencies. +export async function watchDeps() { const localPackagePath = resolvePath('package.json'); // Rotate through different prefix colors to make it easier to differenciate between different deps @@ -66,6 +59,19 @@ export default async (_command: any, args: string[]) => { const newDeps = await getPackageDeps(localPackagePath, PACKAGE_BLACKLIST); await watcher.update(newDeps); }); +} + +/* + * The watch-deps command is meant to improve iteration speed while working in a large monorepo + * with packages that are built independently, meaning packages depends on each other's build output. + * + * The command traverses all dependencies of the current package within the monorepo, and starts + * watching for updates in all those packages. If a change is detected, we stop listening for changes, + * and instead start up watch mode for that package. Starting watch mode means running the first + * available yarn script out of "build:watch", "watch", or "build" --watch. + */ +export default async (_command: any, args: string[]) => { + await watchDeps(); if (args?.length) { await waitForExit(startChild(args)); diff --git a/packages/cli/src/helpers/run.ts b/packages/cli/src/helpers/run.ts index b3ce7ad2c1..3eec19a148 100644 --- a/packages/cli/src/helpers/run.ts +++ b/packages/cli/src/helpers/run.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import { SpawnSyncOptions, spawn, ChildProcess } from 'child_process'; +import { SpawnOptions, spawn, ChildProcess } from 'child_process'; import { ExitCodeError } from './errors'; +type SpawnOptionsPartialEnv = Omit & { + env?: Partial; +}; + // Runs a child command, returning a promise that is only resolved if the child exits with code 0. export async function run( name: string, args: string[] = [], - options: SpawnSyncOptions = {}, + options: SpawnOptionsPartialEnv = {}, ) { const env: NodeJS.ProcessEnv = { ...process.env, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fc1369cde2..4797cceaf2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -19,11 +19,12 @@ import chalk from 'chalk'; import fs from 'fs'; import createPluginCommand from './commands/createPlugin'; import watch from './commands/watch-deps'; -import appLint from './commands/app/lint'; +import lintCommand from './commands/lint'; +import testCommand from './commands/testCommand'; +import appBuild from './commands/app/build'; +import appServe from './commands/app/serve'; import pluginBuild from './commands/plugin/build'; -import pluginLint from './commands/plugin/lint'; import pluginServe from './commands/plugin/serve'; -import pluginTest from './commands/plugin/testCommand'; import { exitWithError } from './helpers/errors'; const main = (argv: string[]) => { @@ -32,10 +33,14 @@ const main = (argv: string[]) => { program.name('backstage-cli').version(packageJson.version ?? '0.0.0'); program - .command('app:lint') - .option('--fix', 'Attempt to automatically fix violations') - .description('Lint an app') - .action(actionHandler(appLint)); + .command('app:build') + .description('Build an app for a production release') + .action(actionHandler(appBuild)); + + program + .command('app:serve') + .description('Serve an app for local development') + .action(actionHandler(appServe)); program .command('create-plugin') @@ -48,23 +53,23 @@ const main = (argv: string[]) => { .description('Build a plugin') .action(actionHandler(pluginBuild)); - program - .command('plugin:lint') - .option('--fix', 'Attempt to automatically fix violations') - .description('Lint a plugin') - .action(actionHandler(pluginLint)); - program .command('plugin:serve') .description('Serves the dev/ folder of a plugin') .action(actionHandler(pluginServe)); program - .command('plugin:test') + .command('lint') + .option('--fix', 'Attempt to automatically fix violations') + .description('Lint a package') + .action(actionHandler(lintCommand)); + + program + .command('test') .option('--watch', 'Enable watch mode') .option('--coverage', 'Report test coverage') - .description('Run all tests for a plugin') - .action(actionHandler(pluginTest)); + .description('Run all tests for package') + .action(actionHandler(testCommand)); program .command('watch-deps') diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 2b0f66ea2f..c6587f326e 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -7,11 +7,12 @@ "private": false, "scripts": { "build": "backstage-cli plugin:build", - "lint": "backstage-cli plugin:lint", - "test": "backstage-cli plugin:test" + "lint": "backstage-cli lint", + "test": "backstage-cli test" }, "devDependencies": { - "@spotify-backstage/cli": "^{{version}}" + "@spotify-backstage/cli": "^{{version}}", + "@types/testing-library__jest-dom": "5.0.2" }, "dependencies": { "@material-ui/lab": "4.0.0-alpha.45" diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 79c292512c..69884c6a44 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -25,7 +25,7 @@ describe('ExampleComponent', () => { const rendered = render( - + , ); expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); }); diff --git a/packages/cli/templates/default-plugin/jest.setup.ts b/packages/cli/templates/default-plugin/src/setupTests.ts similarity index 100% rename from packages/cli/templates/default-plugin/jest.setup.ts rename to packages/cli/templates/default-plugin/src/setupTests.ts diff --git a/packages/cli/templates/default-plugin/jest.config.js b/packages/core/jest.config.ts similarity index 100% rename from packages/cli/templates/default-plugin/jest.config.js rename to packages/core/jest.config.ts diff --git a/packages/core/package.json b/packages/core/package.json index ba4186339a..b6ceeb42f2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,11 +7,10 @@ "types": "dist/cjs/index.d.ts", "scripts": { "build": "tsc --outDir dist/cjs --noEmit false --module CommonJS", - "lint": "web-scripts lint", - "test": "web-scripts test" + "lint": "backstage-cli lint", + "test": "backstage-cli test" }, "dependencies": { - "@spotify-backstage/cli": "^0.1.0", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@types/google-protobuf": "^3.7.2", @@ -27,7 +26,7 @@ "recompose": "0.30.0" }, "devDependencies": { - "@spotify/web-scripts": "^6.0.0", + "@spotify-backstage/cli": "^0.1.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/packages/core/src/layout/Page/PageThemeProvider.ts b/packages/core/src/layout/Page/PageThemeProvider.ts index 64c90b78a1..64f8a9f8a7 100644 --- a/packages/core/src/layout/Page/PageThemeProvider.ts +++ b/packages/core/src/layout/Page/PageThemeProvider.ts @@ -45,7 +45,7 @@ export const gradients: Record = { colors: ['#F13DA2', '#FF8A48'], }, purpleBlue: { - colors: ['#4100F4', '#AF2996'], + colors: ['#2D00AA', '#C769B5'], }, tealGreen: { colors: ['#19E68C', '#1D7F6E'], diff --git a/plugins/home-page/package.json b/plugins/home-page/package.json index af515ae646..5380d8d25b 100644 --- a/plugins/home-page/package.json +++ b/plugins/home-page/package.json @@ -11,6 +11,7 @@ "@testing-library/user-event": "^7.1.2", "@types/jest": "^24.0.0", "@types/node": "^12.0.0", + "@types/testing-library__jest-dom": "5.0.2", "react": "^16.12.0", "react-dom": "^16.12.0", "@material-ui/core": "^4.9.1", @@ -18,8 +19,8 @@ }, "scripts": { "build": "backstage-cli plugin:build", - "lint": "backstage-cli plugin:lint", - "test": "backstage-cli plugin:test" + "lint": "backstage-cli lint", + "test": "backstage-cli test" }, "license": "Apache-2.0" } diff --git a/plugins/home-page/jest.setup.ts b/plugins/home-page/src/setupTests.ts similarity index 100% rename from plugins/home-page/jest.setup.ts rename to plugins/home-page/src/setupTests.ts diff --git a/plugins/welcome/jest.config.js b/plugins/welcome/jest.config.js deleted file mode 100644 index 2e584f12dc..0000000000 --- a/plugins/welcome/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -module.exports = { - ...require('@spotify/web-scripts/config/jest.config.js'), - setupFilesAfterEnv: ['../jest.setup.ts'], -}; diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 3a75a62755..1d5f6a7373 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -7,12 +7,13 @@ "private": false, "scripts": { "build": "backstage-cli plugin:build", - "lint": "backstage-cli plugin:lint", - "test": "backstage-cli plugin:test" + "lint": "backstage-cli lint", + "test": "backstage-cli test" }, "devDependencies": { "@spotify-backstage/core": "^0.1.0", - "@spotify-backstage/cli": "^0.1.0" + "@spotify-backstage/cli": "^0.1.0", + "@types/testing-library__jest-dom": "5.0.2" }, "dependencies": { "@material-ui/lab": "4.0.0-alpha.45" diff --git a/plugins/welcome/jest.setup.ts b/plugins/welcome/src/setupTests.ts similarity index 100% rename from plugins/welcome/jest.setup.ts rename to plugins/welcome/src/setupTests.ts diff --git a/yarn.lock b/yarn.lock index 2c7691f88d..dda14de215 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3057,6 +3057,14 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "25.1.4" + resolved "https://registry.npmjs.org/@types/jest/-/jest-25.1.4.tgz#9e9f1e59dda86d3fd56afce71d1ea1b331f6f760" + integrity sha512-QDDY2uNAhCV7TMCITrxz+MRk1EizcsevzfeS6LykIlq2V1E5oO4wXG8V2ZEd9w7Snxeeagk46YbMgZ8ESHx3sw== + dependencies: + jest-diff "^25.1.0" + pretty-format "^25.1.0" + "@types/jest@^24.0.0": version "24.9.1" resolved "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" @@ -3231,6 +3239,13 @@ dependencies: pretty-format "^24.3.0" +"@types/testing-library__jest-dom@5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.2.tgz#89b782e0f187fe1e80d6375133da74182ba02065" + integrity sha512-dZP+/WHndgCSmdaImITy0KhjGAa9c0hlGGkzefbtrPFpnGEPZECDA0zyvfSp8RKhHECJJSKHFExjOwzo0rHyIA== + dependencies: + "@types/jest" "*" + "@types/testing-library__react@^9.1.2": version "9.1.2" resolved "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.2.tgz#e33af9124c60a010fc03a34eff8f8a34a75c4351"