diff --git a/ADOPTERS.md b/ADOPTERS.md index 75815f32d2..5551a91c41 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,7 +1,8 @@ -| Organization | Contact | Description of Use | -| ---------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@alund](https://github.com/alund) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| Organization | Contact | Description of Use | +| ---------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@alund](https://github.com/alund) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dca9e678dc..6200f3235b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,10 @@ If you are proposing a feature: - Remember that this is a volunteer-driven project, and that contributions are welcome :) +## Add your company to ADOPTERS + +Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project. + # Get Started! So...feel ready to jump in? Let's do this. Head over to the [Getting Started guide](https://github.com/spotify/backstage#getting-started) 👏🏻💯 diff --git a/README.md b/README.md index 07d523a6ae..8e7574e128 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,9 @@ The Backstage platform consists of a number of different components: - **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md). - [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - 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 API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_. - [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph. -- **proxy** \* - Terminates HTTPS and exposes any RESTful API to Plugins. +- [**proxy**](https://github.com/spotify/backstage/tree/master/plugins/proxy-backend) - Terminates HTTPS and exposes any RESTful API to Plugins. - **identity** - A backend service that holds your organisation's metadata. -_\* not yet released_ - ## Getting started To run a Backstage app, you will need to have the following installed: @@ -86,17 +84,16 @@ And that's it! You are good to go 👍 ### Next step -Take a look at the [Getting Started](docs/getting-started/README.md) guide to learn more about how to extend the functionality with Plugins. +Take a look at the [Getting Started](docs/getting-started/index.md) guide to learn how to set up Backstage, and how to develop on the platform. ## Documentation -- [Getting Started](docs/getting-started/README.md) -- [Create a Backstage App](docs/create-an-app.md) -- [Architecture](docs/overview/architecture-terminology.md) ([Decisions](docs/architecture-decisions)) -- [API references](docs/reference/README.md) -- [Designing for Backstage](docs/design.md) +- [Main documentation](docs/README.md) +- [Service Catalog](docs/features/software-catalog/index.md) +- [Create a Backstage App](docs/getting-started/create-an-app.md) +- [Architecture](docs/overview/architecture-terminology.md) ([Decisions](docs/architecture-decisions/index.md)) +- [Designing for Backstage](docs/dls/design.md) - [Storybook - UI components](http://storybook.backstage.io) -- [Contributing to Storybook](docs/getting-started/contributing-to-storybook.md) ## Contributing @@ -109,6 +106,7 @@ We would love your help in building Backstage! See [CONTRIBUTING](CONTRIBUTING.m - [RFCs](https://github.com/spotify/backstage/labels/rfc) - Help shape the technical direction - [FAQ](docs/FAQ.md) - Frequently Asked Questions - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll +- [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates - [Newsletter](https://mailchi.mp/spotify/backstage-community) - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ diff --git a/docs/README.md b/docs/README.md index f8385fb4c8..904fdf3356 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ better yet, a pull request. - [Create an App](getting-started/create-an-app.md) - App configuration - [Configuring App with plugins](getting-started/configure-app-with-plugins.md) - - [Customize the look-and-feel of your App](getting-started/customize-app-look-and-feel.md) + - [Customize the look-and-feel of your App](getting-started/app-custom-theme.md) - Deployment scenarios - [Kubernetes](getting-started/deployment-k8s.md) - [Other](getting-started/deployment-other.md) @@ -32,8 +32,12 @@ better yet, a pull request. - [API](features/software-catalog/api.md) - Software creation templates - [Overview](features/software-templates/index.md) - - [Configure templates](features/software-templates/configure-templates.md) - [Adding templates](features/software-templates/adding-templates.md) + - Extending the Scaffolder: + - [Overview](features/software-templates/extending/index.md) + - [Create your own Templater](features/software-templates/extending/create-your-own-templater.md) + - [Create your own Publisher](features/software-templates/extending/create-your-own-publisher.md) + - [Create your own Preparer](features/software-templates/extending/create-your-own-preparer.md) - Docs-like-code - [Overview](features/techdocs/README.md) - [Getting Started](features/techdocs/getting-started.md) @@ -48,7 +52,7 @@ better yet, a pull request. - [Overview](plugins/index.md) - [Existing plugins](plugins/existing-plugins.md) - [Creating a new plugin](plugins/create-a-plugin.md) - - [Developing a plugin](plugins/developing-plugins.md) + - [Developing a plugin](plugins/plugin-development.md) - [Structure of a plugin](plugins/structure-of-a-plugin.md) - Backends and APIs - [Proxying](plugins/proxying.md) @@ -67,7 +71,7 @@ better yet, a pull request. - [Glossary](auth/glossary.md) - Designing for Backstage - [Backstage Design Language System (DLS)](dls/design.md) - - [Storybook -- reusable UI components](dls/storybook.md) + - [Storybook -- reusable UI components](http://storybook.backstage.io) - [Contributing to Storybook](dls/contributing-to-storybook.md) - [Figma resources](dls/figma.md) - API references diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md index 981ae18057..d348dd2f93 100644 --- a/docs/auth/oauth.md +++ b/docs/auth/oauth.md @@ -9,10 +9,11 @@ to various third party APIs. There are occasions when the user wants to perform actions towards third party services that require authorization via OAuth. Backstage provides standardized [Utility APIs](../api/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. +[GoogleAuthApi](https://github.com/spotify/backstage/blob/master/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](https://github.com/spotify/backstage/tree/master/plugins/auth-backend) +plugin to provide a popup-based OAuth flow. ## Background @@ -51,8 +52,9 @@ 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) +[auth-backend](https://github.com/spotify/backstage/tree/master/plugins/auth-backend) +and +[DefaultAuthConnector](https://github.com/spotify/backstage/blob/master/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 diff --git a/docs/dls/storybook.md b/docs/dls/storybook.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index a9fc17ce75..68cee2352c 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -311,3 +311,114 @@ component, but there will always be one ultimate owner. Apart from being a string, the software catalog leaves the format of this field open to implementers to choose. Most commonly, it is set to the ID or email of a group of people in an organizational structure. + +## Kind: Template + +Describes the following entity kind: + +| Field | Value | +| -------------------- | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `Kind: Templatekind` | `Template` | + +A Template describes a skeleton for use with the Scaffolder. It is used for +describing what templating library is supported, and also for documenting the +variables that the template requires using +[JSON Forms Schema](https://jsonforms.io/). + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + name: react-ssr-template + title: React SSR Template + description: + Next.js application skeleton for creating isomorphic web applications. + tags: + - Recommended + - React +spec: + owner: web@example.com + templater: cookiecutter + type: website + path: '.' + schema: + required: + - component_id + - description + properties: + component_id: + title: Name + type: string + description: Unique name of the component + description: + title: Description + type: string + description: Description of the component +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `Template`, respectively. + +### `metadata.title` [required] + +The nice display name for the template as a string, e.g. `React SSR Template`. +This field is required as is used to reference the template to the user instead +of the `metadata.name` field. + +### `metadata.tags` [optional] + +A list of strings that can be associated with the template, e.g. +`['Recommended', 'React']`. + +This list will also be used in the frontend to display to the user so you can +potentially search and group templates by these tags. + +### `spec.type` [optional] + +The type of component as a string, e.g. `website`. This field is optional but +recommended. + +The software catalog accepts any type value, but an organisation should take +great care to establish a proper taxonomy for these. Tools including Backstage +itself may read this field and behave differently depending on its value. For +example, a website type component may present tooling in the Backstage interface +that is specific to just websites. + +The current set of well-known and common values for this field is: + +- `service` - a backend service, typically exposing an API +- `website` - a website +- `library` - a software library, such as an NPM module or a Java library + +### `spec.templater` [required] + +The templating library that is supported by the template skeleton as a string, +e.g `cookiecutter`. + +Different skeletons will use different templating syntax, so it's common that +the template will need to be run with a particular piece of software. + +This key will be used to identify the correct templater which is registered into +the `TemplatersBuilder`. + +The values which are available by default are: + +- `cookiecutter` - [cookiecutter](https://github.com/cookiecutter/cookiecutter). + +### `spec.path` [optional] + +The string location where the templater should be run if it is not on the same +level as the `template.yaml` definition, e.g. `./cookiecutter/skeleton`. + +This will set the `cwd` when running the templater to the folder path that you +specify relative to the `template.yaml` definition. + +This is also particularly useful when you have multiple template definitions in +the same repository but only a single `template.yaml` registered in backstage. diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index daade46b4d..4925f32e1a 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -21,4 +21,6 @@ can be organised around the entities in the catalog. ## Using the Service Catalog +TODO + ![](service-catalog-home.png) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index e69de29bb2..e929ec4a97 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -0,0 +1,102 @@ +# Adding your own Templates + +Templates are stored in the **Service Catalog** under a kind `Template`. The +minimum that the a template skeleton needs is a `template.yaml` but it would be +good to also have some files in there that can be templated in. + +A simple `template.yaml` definition might look something like this: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + # unique name per namespace for the template + name: react-ssr-template + # title of the template + title: React SSR Template + # a description of the template + description: Next.js application skeleton for creating isomorphic web applications. + # some tags to display in the frontend + tags: + - Recommended + - React +spec: + # which templater key to use in the templaters builder + templater: cookiecutter + # what does this template create + type: website + # if the template is not in the current directory where this definition is kept then specfiy + path: './template' + # the schema for the form which is displayed in the frontend. + # should follow JSON schema for forms: https://jsonforms.io/ + schema: + required: + - component_id + - description + properties: + component_id: + title: Name + type: string + description: Unique name of the component + description: + title: Description + type: string + description: Description of the component +``` + +[Template Entity](../software-catalog/descriptor-format.md#kind-template) +contains more information about the required fields. + +Once we have a `template.yaml` ready, we can then add it to the service catalog +for use by the scaffolder. + +Currently the catalog supports loading definitions from Github + Local Files. To +load from other places, not only will there need to be another preparer, but the +support to load the location will also need to be added to the Catalog. + +For loading from a file the following command should work when the backend is +running: + +```sh +curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}" +``` + +If loading from a git location, you can run the following + +```sh +curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"github\", \"target\": \"https://${YOUR GITHUB REPO}blob/master/${PATH TO FOLDER}/template.yaml\"}" +``` + +This should then have added the catalog, and also should now be listed under the +create page at http://localhost:3000/create. + +Alternatively, if you want to get setup with some mock templates that are +already provided for you, you can run the following to load those templates: + +``` +yarn lerna run mock-data +``` + +The `type` field which is chosen in the request to add the `template.yaml` to +the Service Catalog here, will be come the `PreparerKey` which will be used to +select the `Preparer` when creating a job. + +### Adding form values in the Scaffolder Wizard + +The `spec.schema` property in the +[Template Entity](../software-catalog/descriptor-format.md#kind-template) is a +`yaml` version of the JSON Form Schema standard. + +Here you can define the key/values and then the wizard will convert this to a +form for the user to fill in when your template is selected. + +You can find out much more about the standard and how to use it here: +https://jsonforms.io diff --git a/docs/features/software-templates/assets/added-to-the-catalog-list.png b/docs/features/software-templates/assets/added-to-the-catalog-list.png new file mode 100644 index 0000000000..4b544e51c1 Binary files /dev/null and b/docs/features/software-templates/assets/added-to-the-catalog-list.png differ diff --git a/docs/features/software-templates/assets/complete.png b/docs/features/software-templates/assets/complete.png new file mode 100644 index 0000000000..eee14fae0e Binary files /dev/null and b/docs/features/software-templates/assets/complete.png differ diff --git a/docs/features/software-templates/assets/create.png b/docs/features/software-templates/assets/create.png new file mode 100644 index 0000000000..8a2e92b9f4 Binary files /dev/null and b/docs/features/software-templates/assets/create.png differ diff --git a/docs/features/software-templates/assets/failed.png b/docs/features/software-templates/assets/failed.png new file mode 100644 index 0000000000..bca8c72d6a Binary files /dev/null and b/docs/features/software-templates/assets/failed.png differ diff --git a/docs/features/software-templates/assets/go-to-catalog.png b/docs/features/software-templates/assets/go-to-catalog.png new file mode 100644 index 0000000000..ae16230a02 Binary files /dev/null and b/docs/features/software-templates/assets/go-to-catalog.png differ diff --git a/docs/features/software-templates/assets/running.png b/docs/features/software-templates/assets/running.png new file mode 100644 index 0000000000..208376e059 Binary files /dev/null and b/docs/features/software-templates/assets/running.png differ diff --git a/docs/features/software-templates/assets/template-picked-2.png b/docs/features/software-templates/assets/template-picked-2.png new file mode 100644 index 0000000000..685e356ee8 Binary files /dev/null and b/docs/features/software-templates/assets/template-picked-2.png differ diff --git a/docs/features/software-templates/assets/template-picked.png b/docs/features/software-templates/assets/template-picked.png new file mode 100644 index 0000000000..1094acec4a Binary files /dev/null and b/docs/features/software-templates/assets/template-picked.png differ diff --git a/docs/features/software-templates/configure-templates.md b/docs/features/software-templates/configure-templates.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/features/software-templates/extending/create-your-own-preparer.md b/docs/features/software-templates/extending/create-your-own-preparer.md new file mode 100644 index 0000000000..4e45bef535 --- /dev/null +++ b/docs/features/software-templates/extending/create-your-own-preparer.md @@ -0,0 +1,97 @@ +# Create your own Preparer + +Preparers are responsible for reading the location of the definition of a +[Template Entity](../../software-catalog/descriptor-format.md#kind-template) and +making a temporary folder with the contents of the selected skeleton. + +Currently, we provide two different providers that can parse two different +location protocols: + +- `file://` +- `github://` + +These two are added to the `PreparersBuilder` and then passed into the +`createRouter` function of the `@spotify/plugin-scaffolder-backend` + +An full example backend can be found +[here](https://github.com/spotify/backstage/blob/d91c10f654475a60829fa33a5c81018e517a319a/packages/backend/src/plugins/scaffolder.ts), +but it looks something like the following + +```ts +import { + createRouter, + FilePreparer, + GithubPreparer, + Preparers, +} from '@backstage/plugin-scaffolder-backend'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + const preparers = new Preparers(); + + const filePreparer = new FilePreparer(); + const githubPreparer = new GithubPreparer(); + + preparers.register('file', filePreparer); + preparers.register('github', githubPreparer); + + return await createRouter({ + preparers, + templaters, + logger, + }); +} +``` + +As you can see in the above code, a `PreparerBuilder` is created, and then two +of the `preparers` are registered with the different protocols that they accept. + +The `protocol` is set on the +[Template Entity](../../software-catalog/descriptor-format.md#kind-template) +when added to the service catalog. You can see more about this `PreparerKey` +here in [Register your own template](../adding-templates.md) + +**note:** Currently the catalog supports loading definitions from Github + Local +Files, which translate into the two `PreparerKeys` `file` and `github`. To load +from other places, not only will there need to be another preparer, but the +support to load the location will also need to be added to the Catalog. + +### Creating your own Preparer to add to the `PreparerBuilder` + +All preparers need to implement the `PreparerBase` type. + +That type looks like the following: + +```ts +export type PreparerBase = { + prepare( + template: TemplateEntityV1alpha1, + opts: { logger: Logger }, + ): Promise; +}; +``` + +The `prepare` function will be given the +[Template Entity](../../software-catalog/descriptor-format.md#kind-template) +along with the source of where the `template.yaml` was loaded from under the +`metedata.annotations.managed-by-location` property. + +Now it's up to you to implement a function which can go and fetch the skeleton +and put the contents into a temporary directory and return that directory path. + +Some good examples exist here: + +- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts +- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts + +### Registerinng your own Preparer + +You can register the preparer that you have created with the `PreparerBuilder` +by using the `PreparerKey` from the Catalog, for example like this: + +```ts +const preparers = new Preparers(); +preparers.register('gcs', new GoogleCloudStoragePreparer()); +``` + +And then pass this in to the `createRouter` function. diff --git a/docs/features/software-templates/extending/create-your-own-publisher.md b/docs/features/software-templates/extending/create-your-own-publisher.md new file mode 100644 index 0000000000..8baf604e01 --- /dev/null +++ b/docs/features/software-templates/extending/create-your-own-publisher.md @@ -0,0 +1,92 @@ +# Create your own Publisher + +Publishers are responsible for pushing and storing the templated skeleton after +the values have been templated by the `Templater`. See +[Create your own templater](./create-your-own-templater.md) for more info. + +They recieve a directory or location where the templater has sucessfully run on, +and is now ready to store somewhere. They also get given some other options +which are sent from the frontend, such as the `storePath` which is a string of +where the frontend thinks we should save this templated folder. + +Currently we provide the following `publishers`: + +- `github` + +This publisher is passed through to the `createRouter` function of the +`@spotify/plugin-scaffolder-backend`. Currently only one publisher is supported, +but PR's are always welcome. + +An full example backend can be found +[here](https://github.com/spotify/backstage/blob/d91c10f654475a60829fa33a5c81018e517a319a/packages/backend/src/plugins/scaffolder.ts), +but it looks something like the following + +```ts +import { + createRouter, + GithubPublisher, +} from '@backstage/plugin-scaffolder-backend'; +import { Octokit } from '@octokit/rest'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); + const publisher = new GithubPublisher({ client: githubClient }); + + return await createRouter({ + publisher, + logger, + }); +} +``` + +The publisher will always be called with the location from the selected +`Preparer`. + +### Create your own Publisher and register it with the Scaffolder + +All `publishers` need to implement the `PublisherBase` type. + +That type looks like the following: + +```ts +export type PublisherBase = { + publish(opts: { + entity: TemplateEntityV1alpha1; + values: RequiredTemplateValues & Record; + directory: string; + }): Promise<{ remoteUrl: string }>; +}; +``` + +The `publisher` function will be called with an `options` object which contains +the following: + +- `entity` - the + [Template Entity](../../software-catalog/descriptor-format.md#kind-template) + which is currently being scaffolded +- `values` - a json object which will resemble the `spec.schema` from the + [Template Entity](../../software-catalog/descriptor-format.md#kind-template) + which is defined here under spec.schema`. More info can be found here + [Register your own template](../adding-templates.md#adding-form-values-in-the-scaffolder-wizard) +- `directory` - a string containing the returned path from the `templater`. See + more information here in + [Create your own templater](./create-your-own-templater.md) + +Now it's up to you to implement the `publish` function and return +`{ remoteUrl: string }` which can be used to identify the finished product. + +Some good examples exist here: + +- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts + +### Registering your own Publisher + +Currently, we only support one `publisher` (PR's welcome), but you can register +any single `publisher` with the `createRouter`. + +```ts +return await createRouter({ + publisher: new MyGitlabPublisher(), +}); +``` diff --git a/docs/features/software-templates/extending/create-your-own-templater.md b/docs/features/software-templates/extending/create-your-own-templater.md new file mode 100644 index 0000000000..947190c5e5 --- /dev/null +++ b/docs/features/software-templates/extending/create-your-own-templater.md @@ -0,0 +1,146 @@ +# Creating your own Templater + +Templaters are responsible for taking the directory path for the skeleton +returned by the preparers, and then executing the templating command on top of +the file and returning the completed template path. This may or may not be the +same directory as the input directory. + +They also recieve additional values from the frontend, which can be used to +interpolate into the skeleton files. + +Currently we provide the following templaters: + +- `cookiecutter` + +This templater is added the `TemplaterBuilder` and then passed into the +`createRouter` function of the `@spotify/plugin-scaffolder-backend` + +An full example backend can be found +[here](https://github.com/spotify/backstage/blob/d91c10f654475a60829fa33a5c81018e517a319a/packages/backend/src/plugins/scaffolder.ts), +but it looks something like the following + +```ts +import { + CookieCutter, + createRouter, + Templaters, +} from '@backstage/plugin-scaffolder-backend'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin({ logger }: PluginEnvironment) { + const templaters = new Templaters(); + const cookiecutterTemplater = new CookieCutter(); + templaters.register('cookiecutter', cookiecutterTemplater); + + return await createRouter({ + templaters, + }); +} +``` + +As you can see in the above code a `TemplaterBuilder` is created and the default +`cookiecutter` `templater` is registered under the key `cookicutter`. + +This `TemplaterKey` is used to select the correct templater from the +`spec.templater` in the +[Template Entity](../../software-catalog/descriptor-format.md#kind-template). + +If you wish to add a new templater you'll need to register it with the +`TemplaterBuilder`. + +### Creating your own Templater to add to the `TemplaterBuilder` + +All templaters need to implement the `TemplaterBase` type. + +That type looks like the following: + +```ts +export type TemplaterRunOptions = { + directory: string; + values: RequiredTemplateValues & Record; + logStream?: Writable; + dockerClient: Docker; +}; + +export type TemplaterBase = { + run(opts: TemplaterRunOptions): Promise; +}; +``` + +The `run` function will be given a `TemplaterRunOptions` object which is as +follows: + +- `directory`- the skeleton directory returned from the `Preparer`, more info at + [Create your own preparer](./create-your-own-preparer.md). +- `values` - a json object which will resemble the `spec.schema` from the + [Template Entity](../../software-catalog/descriptor-format.md#kind-template) + which is defined here under spec.schema`. More info can be found here + [Register your own template](../adding-templates.md#adding-form-values-in-the-scaffolder-wizard) +- `logStream` - a stream that you can write to for displaying in the frontend. +- `dockerClient` - a [dockerode](https://github.com/apocas/dockerode) client to + be able to run docker containers. + +_note_ currently the templaters that we provide are basically docker action +containers that are run on top of the skeleton folder. This keeps dependencies +to a minimal for running backstage scaffolder, but you don't /have/ to use +docker. You could create your own templater that spins up an EC2 instance and +downloads the folder and does everything using an AMI if you want. It's entirely +up to you! + +Now it's up to you to implement the `run` function, and then return a +`TemplaterRunResult` which is `{ resultDir: string }`. + +Some good examples exist here: + +- https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts + +### Registering your own Templater + +If you try to process a +[Template Entity](../../software-catalog/descriptor-format.md#kind-template) +with a new `spec.templater` value, you'll need to register that with the +`TemplaterBuilder`. + +For example let's say you have the following +[Template Entity](../../software-catalog/descriptor-format.md#kind-template): + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Template +metadata: + name: react-ssr-template + title: React SSR Template + description: + Next.js application skeleton for creating isomorphic web applications. + tags: + - Recommended + - React +spec: + owner: web@example.com + templater: handlebars + type: website + path: '.' + schema: + required: + - component_id + - description + properties: + component_id: + title: Name + type: string + description: Unique name of the component + description: + title: Description + type: string + description: Description of the component +``` + +You see that the `spec.templater` is set as `handlebars`, you'll need to +register this with the `TemplaterBuilder` like so: + +```ts +const templaters = new Templaters(); +templaters.register('handlebars', new HandlebarsTemplater()); +``` + +And then pass this in to the `createRouter` function. diff --git a/docs/features/software-templates/extending/index.md b/docs/features/software-templates/extending/index.md new file mode 100644 index 0000000000..a55128c57d --- /dev/null +++ b/docs/features/software-templates/extending/index.md @@ -0,0 +1,87 @@ +## Extending the Scaffolder + +Welcome. Take a seat. You're at the Scaffolder Documentation. + +So - You wanna create stuff inside your company from some prebaked templates? +You're at the right place. + +This guide is gonna take you through how the Scaffolder in Backstage works. +We'll dive into some jargon and run through whats going on in the backend to be +able to create these templates. There's also more guides that you might find +useful at the bottom of this document. At it's core, theres 3 simple stages. + +1. Pick a skeleton +2. Template some variables into the skeleton +3. Send the templated skeleton somewhere + +These three steps are translated to the folllowing stages under the hood in the +scaffolder that you will need to know: + +1. Prepare +2. Template +3. Publish + +Each of these steps can be configured for your own use case, but we provide some +sensible defaults too. + +Lets dive a little deeper into these phases. + +### Glossary and Jargon + +**Preparer** - The preparer is responsible for fetching the skeleton code and +placing it into a directory and then will return that directory. It is +registered with the `Preparers` with a particular type, which is then used in +the router to pick the correct `Preparer` to run for the `Template` entity. + +**Templater** - The templater is responsible for actually running the chosen +templater on top of the previously returned temporary directory from the +**Preprarer**. We advise making these docker containers as it can keep all +dependencies, for example Cookiecutter, self contained and not a dependency on +the host machine. + +**Publisher** - The publisher is responsible for taking the finished directory, +and publishing it to a remote registry. This could be a Git repository or +something similar. Right now, the scaffolder only supports one publishing method +for the entire lifecycle, but it could be configured from the frontend and +passed through to the scaffolder backend. + +### How it works + +The main of the heavy lifting is done in the +[router.ts](https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/src/service/router.ts#L93) +file in the `scaffolder-backend` plugin. + +There are 2 routes defined in the router. `POST /v1/jobs` and +`GET /v1/job/:jobId` + +To create a scaffolding job, a JSON object containing the +[Template Entity](../../software-catalog/descriptor-format.md#kind-template) + +additional templating values must be posted as the post body. + +```js +{ + "template": { + "apiVersion": "backstage/v1alpha1", + "kind": "Template", + // more stuff here + }, + "values": { + "component_id": "test", + "description": "somethingelse" + } +} +``` + +The values should represent something that is valid with the `schema` part of +the [Template Entity](../../software-catalog/descriptor-format.md#kind-template) + +Once that has been posted, a job will be setup with different stages. And the +job processor will complete each stage before moving onto the next stage, whilst +collecting logs and mutating the running job. + +Here's some futher reading that you might find useful: + +- [Adding your own Template](../adding-templates.md) +- [Creating your own Templater](./create-your-own-templater.md) +- [Creating your own Publisher](./create-your-own-publisher.md) +- [Creating your own Preparer](./create-your-own-preparer.md) diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index e69de29bb2..c7358518cd 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -0,0 +1,59 @@ +# Software Templates + +The Software Templates part of Backstage is a tool that can help you create +Components inside Backstage. It by default has the ability to load skeletons of +code, template in some variables and then publish the template to some location +like GitHub. + +### Getting Started + +The Software Templates are available under `/create`, and if you've followed +[Getting Started with Backstage](../../getting-started), you should be able to +reach `http://localhost:3000/create`. + +You should get something that looks similar to this: + +![Create Image](./assets/create.png) + +### Choose a template + +When you select a template that you want to create, you'll be taken to the next +page which may or may not look different for each template. Each template can +ask for different input variables, and they are then passed to the templater +internally. + +![Enter some variables](./assets/template-picked.png) + +After filling in these variables, you'll get some more fields to fill out which +are required for backstage usage. The owner, which is a `user` in the backstage +system, and the `storePath` which right now must be a Github Organisation and a +non-existing github repository name in the format `organistaion/reponame`. + +![Enter backstage vars](./assets/template-picked-2.png) + +### Run! + +Once you've entered values and confirmed, you'll then get a modal with live +progress of what is currently happening with the creation of your template. + +![Templating Running](./assets/running.png) + +It shouldn't take too long, and you'll have a success screen! + +![Templating Complete](./assets/complete.png) + +If it fails, you'll be able to click on each section to get the log from the +step that failed which can be helpful to debug. + +![Templating failed](./assets/failed.png) + +### View Component in Catalog + +When it's been created you'll see the `View in Catalog` button, which will take +you to the registered component in the catalog: + +![Catalog](./assets/go-to-catalog.png) + +And then you'll also be able to see it in the Catalog View table + +![Catalog](./assets/added-to-the-catalog-list.png) diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index bad55622f9..2ea8fc7fa9 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -1 +1,109 @@ # Getting Started + +> TechDocs is not feature complete and currently you can't set up a complete +> end-to-end working TechDocs plugin without customizing the plugin itself. + +> With TechDocs V.0 you can expect a demonstration of how to integrate docs into +> Backstage. Currently it can create docs using +> [mkdocs](https://www.mkdocs.org/), as well as reading published docs. If you +> publish generated docs and passing in a storageUrl in your `app-config.yaml` +> you can view it in Backstage by going to +> `http://localhost:3000/docs/`. + +Getting started with TechDocs is easy. TechDocs functions as a plugin to +Backstage, why you will need to use Backstage to use TechDocs. + +## What is Backstage? + +Backstage is an open platform for building developer portals. It’s based on the +developer portal we’ve been using internally at Spotify for over four years. +[Read more](https://github.com/spotify/backstage). + +## Prerequisities + +In order to use Backstage and TechDocs, you will need to have the following +installed: + +- [Node.js](https://nodejs.org) Active LTS (long term support), currently v12 +- [Yarn](https://yarnpkg.com/getting-started/install) + +## Creating a new Backstage app + +> If you have already created a Backstage application for this purpose, jump to +> [Installing TechDocs](#installing-techdocs), otherwise complete this step. + +To create a new Backstage application for us to set up TechDocs, you will need +to run the following command: + +```bash +npx @backstage/cli create-app +``` + +You will then be prompted to enter a name for your application. Once you do so, +this will create a new Backstage application for you in a new folder. For +example, if we chose the name `hello-world` for our application, it would create +a new `hello-world` folder containing our new Backstage application. + +## Installing TechDocs + +Inside of our new Backstage application, TechDocs is not provided by default. +For this reason we will need to manually set up TechDocs. It should take less +than a minute. + +### Adding the package + +We will need to add our plugin to your Backstage application. To do so, you can +navigate to your new Backstage application folder and then run a single command +to install TechDocs. + +```bash +cd hello-world/ +``` + +Then you need to navigate to your `packages/app` folder to install TechDocs: + +```bash +cd packages/app +yarn add @backstage/plugin-techdocs +``` + +After a short while, it should successfully install the TechDocs plugin. Now we +just need to set up some basic configuration! + +Enter the following command: + +```bash +yarn install +``` + +Add this to `packages/app/src/plugins.ts`: + +```typescript +export { plugin as TechDocs } from '@backstage/plugin-techdocs'; +``` + +### Setting the configuration + +TechDocs allows for configuration of the docs storage URL through your +app-config file. The URL provided here is demo docs used to testing. + +To use the demo docs, add the following lines to `app-config.yaml`: + +```yaml +techdocs: + storageUrl: https://techdocs-mock-sites.storage.googleapis.com +``` + +## Run Backstage Locally + +Change folder to your Backstage application root. + +```bash +yarn start +``` + +Open browser at [http://localhost:3000/docs/](http://localhost:3000/docs/) + +## Extra Reading + +[Back to Docs](README.md) diff --git a/docs/features/techdocs/publishing-documentation.md b/docs/features/techdocs/publishing-documentation.md index 9d1bac187a..73ac74b458 100644 --- a/docs/features/techdocs/publishing-documentation.md +++ b/docs/features/techdocs/publishing-documentation.md @@ -1 +1,108 @@ # Publishing Documentation + +## Prerequisities + +- [Docker](https://docs.docker.com/get-docker/) +- Static file hosting + +## Create documentation + +Create a directory that contains your documentation. Inside this directory you +should create a file called `mkdocs.yml`. As an example you can create a +directory called `hello-docs` in your home directory (also known as `~`). Below +is a basic example of how it could look. + +The `~/hello-docs/mkdocs.yml` file should have the following content: + +```yaml +site_name: 'example-docs' + +nav: + - Home: index.md + +plugins: + - techdocs-core +``` + +And then the `~/hello-docs/docs/index.md` should have the following content: + +```md +# example docs + +This is a basic example of documentation. +``` + +## Build documentation + +```bash +cd ~/hello-docs/ +docker run -it -w /content -v $(pwd):/content spotify/techdocs build +``` + +You should now have a folder called `~/hello-docs/site/`. + +## Deploy to a file server + +In order to serve documentation to TechDocs, our Backstage plugin needs to +download the HTML rendered from the +[Create documentation](#create-documentation) step above. This will likely exist +on an external file server, or a storage solution such as Google Cloud Storage. + +When deploying documentation, it should be deployed on that file server / +storage solution with the following convention: `{id}/{file}`. For example, if +we wanted to upload the `getting-started/index.html` file for the `backstage` +documentation site, we would upload it to our file server as +`backstage/getting-started/index.html`. + +To explain further how this would look like for multiple documentation sites, +take a look at this example file tree that would be represented on your file +server: + +```md +/backstage/index.html /backstage/getting-started/index.html +/backstage/contributing/index.html /mkdocs/index.html +/mkdocs/plugin-development/index.html +/mkdocs/plugin-development/debugging/index.html +``` + +In this file tree, we have two documentation sites available: `backstage` and +`mkdocs`. Each of them expose several pages. Let's say both of these are hosted +on `http://example.com` as the server URL. + +When you configure the TechDocs plugin in Backstage to use `http://example.com` +as the file server / storage solution, it will translate the following URLs to +the file server: + +| Backstage URL | File Server URL | +| --------------------------------------------------------- | ------------------------------------------------------- | +| https://demo.backstage.io/docs/backstage/ | http://example.com/backstage/index.html | +| https://demo.backstage.io/docs/mkdocs/plugin-development/ | http://example.com/mkdocs/plugin-development/index.html | + +Then deploying new sites is easy. It's as simple as copying over the `site/` +folder produced in the [Create documentation](#create-documentation) step above +and copying it over to the file server / storage solution under the ID of the +documentation site. It will then become immediately available in Backstage under +the same ID as you can see in the table above. + +So, if the URL to your file server is `http://example.com/`, your +`~/hello-docs/site` folder containing the documentation should be accessible at +`http://example.com/hello-docs/`. + +## Configure TechDocs to read from file server + +In order for Backstage to show your documentation, it needs to know where you +uploaded it. + +Make sure you have Backstage set up using +[TechDocs getting started](getting-started.md) + +To point Backstage to your docs storage, add or change the following lines in +your Backstage `app-config.yaml`: + +```yaml +techdocs: + storageUrl: http://example.com +``` + +You can now start Backstage using `yarn start` and open up your browser at +`http://localhost:3000/docs/hello-docs` to view your docs. diff --git a/docs/getting-started/customize-app-look-and-feel.md b/docs/getting-started/customize-app-look-and-feel.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index efceef5892..02729e5bb4 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -50,7 +50,8 @@ system resources and slow things down. ## Package Scripts -There are many commands to be found in the root [package.json](package.json), +There are many commands to be found in the root +[package.json](https://github.com/spotify/backstage/blob/master/package.json), here are some useful ones: ```python @@ -80,7 +81,9 @@ yarn diff # Make sure all plugins are up to date with the latest plugin template yarn create-plugin # Create a new plugin ``` -> See [package.json](/package.json) for other yarn commands/options. +> See +> [package.json](https://github.com/spotify/backstage/blob/master/package.json) +> for other yarn commands/options. [Next Step - Create a Backstage plugin](../plugins/create-a-plugin.md) diff --git a/docs/plugins/create-a-plugin.md b/docs/plugins/create-a-plugin.md index dfc86ef1f5..fa91e38b06 100644 --- a/docs/plugins/create-a-plugin.md +++ b/docs/plugins/create-a-plugin.md @@ -24,7 +24,7 @@ for your new plugin directly by navigating to `http://localhost:3000/my-plugin`._

- my plugin + my plugin

You can also serve the plugin in isolation by running `yarn start` in the plugin diff --git a/docs/plugins/developing-plugins.md b/docs/plugins/developing-plugins.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md index e69de29bb2..bd6fd19691 100644 --- a/docs/plugins/existing-plugins.md +++ b/docs/plugins/existing-plugins.md @@ -0,0 +1,11 @@ +# Existing plugins + +## Open source plugins + +The full list of open source plugins can be found +[here](https://github.com/spotify/backstage/tree/master/plugins). + +## Plugin gallery + +TODO: In the future we would like to have something similar to +https://grafana.com/grafana/plugins diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 0c4f77f299..5021ac9896 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -5,15 +5,14 @@ Backstage is a single-page application composed of a set of plugins. Our goal for the plugin ecosystem is that the definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong -[design guidelines](https://github.com/spotify/backstage/blob/master/docs/design.md) -we ensure the the overall user experience stays consistent between plugins. +[design guidelines](../dls/design.md) we ensure the the overall user experience +stays consistent between plugins. ![plugin](my-plugin_screenshot.png) ## Creating a plugin -To create a plugin, follow the steps outlined -[here](https://github.com/spotify/backstage/blob/master/docs/getting-started/create-a-plugin.md). +To create a plugin, follow the steps outlined [here](create-a-plugin.md). ## Suggesting a plugin diff --git a/docs/plugins/publishing.md b/docs/plugins/publishing.md index 630624f304..76997c30d2 100644 --- a/docs/plugins/publishing.md +++ b/docs/plugins/publishing.md @@ -3,9 +3,10 @@ ## NPM NPM packages are published through CI/CD in the -[.github/workflows/master.yml](../../.github/workflows/master.yml) workflow. -Every commit that is merged to master will be checked for new versions of all -public packages, and any new versions will automatically be published to NPM. +[.github/workflows/master.yml](https://github.com/spotify/backstage/blob/master/.github/workflows/master.yml) +workflow. Every commit that is merged to master will be checked for new versions +of all public packages, and any new versions will automatically be published to +NPM. ### Creating a new release diff --git a/docs/plugins/structure-of-a-plugin.md b/docs/plugins/structure-of-a-plugin.md index 9d15517e03..ceacf578fe 100644 --- a/docs/plugins/structure-of-a-plugin.md +++ b/docs/plugins/structure-of-a-plugin.md @@ -102,6 +102,6 @@ environment you will probably face challenges like CORS policies and/or backend-side authorization. To smooth this process out you can use proxy - either the one you already have (like nginx/haproxy/etc) or the proxy-backend plugin that we provide for the backstage backend. -[Read more](../../plugins/proxy-backend/README.md) +[Read more](https://github.com/spotify/backstage/blob/master/plugins/proxy-backend/README.md) [Back to Getting Started](../README.md) diff --git a/mkdocs.yml b/mkdocs.yml index 7c5a672636..613c9b7216 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,7 +15,7 @@ nav: - Create an App: 'getting-started/create-an-app.md' - App configuration: - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md' - - Customize the look-and-feel of your App: 'getting-started/customize-app-look-and-feel.md' + - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md' - Deployment scenarios: - Kubernetes: 'getting-started/deployment-k8s.md' - Other: 'getting-started/deployment-other.md' @@ -30,8 +30,12 @@ nav: - API: 'features/software-catalog/api.md' - Software creation templates: - Overview: 'features/software-templates/index.md' - - Configure templates: 'features/software-templates/configure-templates.md' - Adding templates: 'features/software-templates/adding-templates.md' + - Extending the Scaffolder: + - Overview: 'features/software-templates/extending/index.md' + - Create your own Templater: 'features/software-templates/extending/create-your-own-templater.md' + - Create your own Publisher: 'features/software-templates/extending/create-your-own-publisher.md' + - Create your own Preparer: 'features/software-templates/extending/create-your-own-preparer.md' - Docs-like-code: - Overview: 'features/techdocs/README.md' - Getting Started: 'features/techdocs/getting-started.md' @@ -46,7 +50,7 @@ nav: - Overview: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' - Creating a new plugin: 'plugins/create-a-plugin.md' - - Developing a plugin: 'plugins/developing-plugins.md' + - Developing a plugin: 'plugins/plugin-development.md' - Structure of a plugin: 'plugins/structure-of-a-plugin.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' @@ -65,7 +69,7 @@ nav: - Glossary: 'auth/glossary.md' - Designing for Backstage: - Backstage Design Language System (DLS): 'dls/design.md' - - Storybook -- reusable UI components: 'dls/storybook.md' + - Storybook -- reusable UI components: 'http://storybook.backstage.io' - Contributing to Storybook: 'dls/contributing-to-storybook.md' - Figma resources: 'dls/figma.md' - API references: diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 80e0bcf26c..23312bd308 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -43,6 +43,9 @@ async function getConfig() { ), }, + // A bit more opinionated + testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'], + // Default behaviour is to not apply transforms for node_modules, but we still want // to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages. transformIgnorePatterns: [ @@ -55,13 +58,37 @@ async function getConfig() { options.setupFilesAfterEnv = ['/setupTests.ts']; } - return { - ...options, + // We read all "jest" config fields in package.json files all the way to the filesystem root. + // All configs are merged together to create the final config, with longer paths taking precedence. + // The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined. + const pkgJsonConfigs = []; + let currentPath = process.cwd(); - // 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, - }; + // Some sanity check to avoid infinite loop + for (let i = 0; i < 100; i++) { + const packagePath = path.resolve(currentPath, 'package.json'); + const exists = fs.pathExistsSync(packagePath); + if (exists) { + try { + const data = fs.readJsonSync(packagePath); + if (data.jest) { + pkgJsonConfigs.unshift(data.jest); + } + } catch (error) { + throw new Error( + `Failed to parse package.json file reading jest configs, ${error}`, + ); + } + } + + const newPath = path.dirname(currentPath); + if (newPath === currentPath) { + break; + } + currentPath = newPath; + } + + return Object.assign(options, ...pkgJsonConfigs); } module.exports = getConfig(); diff --git a/packages/cli/package.json b/packages/cli/package.json index c4bf5a6b4d..55462d1ebf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,6 +54,7 @@ "dashify": "^2.0.0", "diff": "^4.0.2", "eslint": "^7.1.0", + "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-import": "^2.20.2", "eslint-plugin-monorepo": "^0.2.1", "fork-ts-checker-webpack-plugin": "^4.0.5", diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 41e69c51be..b355939e55 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -18,12 +18,12 @@ import { Command } from 'commander'; import { run } from '../lib/run'; import { paths } from '../lib/paths'; -export default async (cmd: Command) => { +export default async (cmd: Command, cmdArgs: string[]) => { const args = [ '--ext=js,jsx,ts,tsx', '--max-warnings=0', - '--format=codeframe', - paths.targetDir, + '--format=eslint-formatter-friendly', + ...(cmdArgs ?? [paths.targetDir]), ]; if (cmd.fix) { args.push('--fix'); diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 5f881bd53c..b498482f8a 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -29,13 +29,16 @@ export const transforms = ( ): Transforms => { const { isDev } = options; + const extraTransforms = isDev ? ['react-hot-loader'] : []; + const loaders = [ { test: /\.(tsx?)$/, exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { - transforms: ['typescript', 'jsx', 'react-hot-loader'], + transforms: ['typescript', 'jsx', ...extraTransforms], + production: !isDev, }, }, { @@ -43,7 +46,8 @@ export const transforms = ( exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { - transforms: ['jsx', 'react-hot-loader'], + transforms: ['jsx', ...extraTransforms], + production: !isDev, }, }, { @@ -51,7 +55,10 @@ export const transforms = ( use: [ { loader: require.resolve('@sucrase/webpack-loader'), - options: { transforms: ['jsx'] }, + options: { + transforms: ['jsx', ...extraTransforms], + production: !isDev, + }, }, { loader: require.resolve('@svgr/webpack'), diff --git a/plugins/README.md b/plugins/README.md index 7fe6a7312d..50f4395bd9 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -4,11 +4,11 @@ Backstage is a single-page application composed of a set of plugins. Our goal for the plugin ecosystem is that the definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://github.com/spotify/backstage/blob/master/docs/design.md) we ensure the the overall user experience stays consistent between plugins. -![plugin](../docs/getting-started/my-plugin_screenshot.png) +![plugin](../docs/plugins/my-plugin_screenshot.png) ## Creating a plugin -To create a plugin, follow the steps outlined [here](https://github.com/spotify/backstage/blob/master/docs/getting-started/create-a-plugin.md). +To create a plugin, follow the steps outlined [here](https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md). ## Suggesting a plugin diff --git a/plugins/auth-backend/src/lib/EnvironmentHandler.ts b/plugins/auth-backend/src/lib/EnvironmentHandler.ts index 6c16b3a712..76c1f40b73 100644 --- a/plugins/auth-backend/src/lib/EnvironmentHandler.ts +++ b/plugins/auth-backend/src/lib/EnvironmentHandler.ts @@ -16,49 +16,56 @@ import express from 'express'; import { AuthProviderRouteHandlers } from '../providers/types'; -import { NotFoundError } from '@backstage/backend-common'; export type EnvironmentHandlers = { [key: string]: AuthProviderRouteHandlers; }; export class EnvironmentHandler implements AuthProviderRouteHandlers { - constructor(private readonly providers: EnvironmentHandlers) {} + constructor( + private readonly providerId: string, + private readonly providers: EnvironmentHandlers, + ) {} - private getProviderForEnv(req: express.Request): AuthProviderRouteHandlers { + private getProviderForEnv( + req: express.Request, + res: express.Response, + ): AuthProviderRouteHandlers | undefined { const env = req.query.env?.toString(); - if (!this.providers.hasOwnProperty(env)) { - throw new NotFoundError( - `No environment for ${env} found in this provider`, - ); + + if (this.providers.hasOwnProperty(env)) { + return this.providers[env]; } - return this.providers[env]; + + res.status(404).send( + `Missing configuration. +
+
+For this flow to work you need to supply a valid configuration for the "${env}" environment of the "${this.providerId}" provider.`, + ); + return undefined; } async start(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - await provider.start(req, res); + const provider = this.getProviderForEnv(req, res); + await provider?.start(req, res); } async frameHandler( req: express.Request, res: express.Response, ): Promise { - const provider = this.getProviderForEnv(req); - await provider.frameHandler(req, res); + const provider = this.getProviderForEnv(req, res); + await provider?.frameHandler(req, res); } async refresh(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - if (provider.refresh) { - await provider.refresh(req, res); - } + const provider = this.getProviderForEnv(req, res); + await provider?.refresh?.(req, res); } async logout(req: express.Request, res: express.Response): Promise { - const provider = this.getProviderForEnv(req); - if (provider.logout) { - await provider.logout(req, res); - } + const provider = this.getProviderForEnv(req, res); + await provider?.logout?.(req, res); } } diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ed06c5786e..bd7d4aa712 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -135,16 +135,16 @@ export function createGithubProvider( logger: Logger, tokenIssuer: TokenIssuer, ) { + const providerId = 'github'; const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { const config = (envConfig as unknown) as OAuthProviderConfig; const { secure, appOrigin } = config; - const callbackURLParam = `?env=${env}`; const opts = { clientID: config.clientId, clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/github/handler/frame${callbackURLParam}`, + callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, }; if (!opts.clientID || !opts.clientSecret) { @@ -163,12 +163,12 @@ export function createGithubProvider( envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), { disableRefresh: true, persistScopes: true, - providerId: 'github', + providerId, secure, baseUrl, appOrigin, tokenIssuer, }); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler(providerId, envProviders); } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 950c414750..af1d57fcce 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -142,6 +142,7 @@ export function createGitlabProvider( logger: Logger, tokenIssuer: TokenIssuer, ) { + const providerId = 'gitlab'; const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { @@ -152,12 +153,10 @@ export function createGitlabProvider( clientSecret, audience, } = (envConfig as unknown) as OAuthProviderConfig; - const callbackURLParam = `?env=${env}`; - const opts = { clientID: clientId, clientSecret: clientSecret, - callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`, + callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, baseURL: audience, }; @@ -176,12 +175,12 @@ export function createGitlabProvider( envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), { disableRefresh: true, - providerId: 'gitlab', + providerId, secure, baseUrl, appOrigin, tokenIssuer, }); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler(providerId, envProviders); } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 04705b66c1..3023c09886 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -154,16 +154,16 @@ export function createGoogleProvider( logger: Logger, tokenIssuer: TokenIssuer, ) { + const providerId = 'google'; const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { const config = (envConfig as unknown) as OAuthProviderConfig; const { secure, appOrigin } = config; - const callbackURLParam = `?env=${env}`; const opts = { clientID: config.clientId, clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/google/handler/frame${callbackURLParam}`, + callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, }; if (!opts.clientID || !opts.clientSecret) { @@ -181,12 +181,12 @@ export function createGoogleProvider( envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), { disableRefresh: false, - providerId: 'google', + providerId, secure, baseUrl, appOrigin, tokenIssuer, }); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler(providerId, envProviders); } diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 1b5ee6d17e..60b6bc605b 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -152,16 +152,16 @@ export function createOAuth2Provider( logger: Logger, tokenIssuer: TokenIssuer, ) { + const providerId = 'oauth2'; const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { const config = (envConfig as unknown) as GenericOAuth2ProviderConfig; const { secure, appOrigin } = config; - const callbackURLParam = `?env=${env}`; const opts = { clientID: config.clientId, clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/oauth2/handler/frame${callbackURLParam}`, + callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, authorizationURL: config.authorizationURL, tokenURL: config.tokenURL, }; @@ -186,7 +186,7 @@ export function createOAuth2Provider( envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), { disableRefresh: false, - providerId: 'oauth2', + providerId, secure, baseUrl, appOrigin, @@ -194,5 +194,5 @@ export function createOAuth2Provider( }); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler(providerId, envProviders); } diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index e73843bb0c..01d7684e97 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -47,17 +47,16 @@ type PrivateInfo = { }; export class OktaAuthProvider implements OAuthProviderHandlers { - private readonly _strategy: any; - /** + /** * Due to passport-okta-oauth forcing options.state = true, * passport-oauth2 requires express-session to be installed * so that the 'state' parameter of the oauth2 flow can be stored. * This implementation of StateStore matches the NullStore found within * passport-oauth2, which is the StateStore implementation used when options.state = false, * allowing us to avoid using express-session in order to integrate with Okta. - */ + */ private _store: StateStore = { store(_req: express.Request, cb: any) { cb(null, null); @@ -65,25 +64,27 @@ export class OktaAuthProvider implements OAuthProviderHandlers { verify(_req: express.Request, _state: string, cb: any) { cb(null, true); }, - } + }; constructor(options: OAuthProviderOptions) { - this._strategy = new OktaStrategy({ - passReqToCallback: false as true, - ...options, - store: this._store, - response_type: 'code', - }, ( - accessToken: any, - refreshToken: any, - params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, - ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); + this._strategy = new OktaStrategy( + { + passReqToCallback: false as true, + ...options, + store: this._store, + response_type: 'code', + }, + ( + accessToken: any, + refreshToken: any, + params: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile, params.id_token); - done( - undefined, + done( + undefined, { providerInfo: { idToken: params.id_token, @@ -96,13 +97,14 @@ export class OktaAuthProvider implements OAuthProviderHandlers { { refreshToken, }, - ) - }); + ); + }, + ); } async start( req: express.Request, - options: Record + options: Record, ): Promise { const providerOptions = { ...options, @@ -113,7 +115,7 @@ export class OktaAuthProvider implements OAuthProviderHandlers { } async handler( - req: express.Request + req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { const { response, privateInfo } = await executeFrameHandlerStrategy< OAuthResponse, @@ -148,7 +150,6 @@ export class OktaAuthProvider implements OAuthProviderHandlers { }, profile, }); - } private async populateIdentity( @@ -173,17 +174,17 @@ export function createOktaProvider( logger: Logger, tokenIssuer: TokenIssuer, ) { + const providerId = 'okta'; const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { const config = (envConfig as unknown) as OAuthProviderConfig; const { secure, appOrigin } = config; - const callbackURLParam = `?env=${env}`; const opts = { audience: config.audience, clientID: config.clientId, clientSecret: config.clientSecret, - callbackURL: `${baseUrl}/okta/handler/frame${callbackURLParam}`, + callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`, }; if (!opts.clientID || !opts.clientSecret || !opts.audience) { @@ -201,7 +202,7 @@ export function createOktaProvider( envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), { disableRefresh: false, - providerId: 'okta', + providerId, secure, baseUrl, appOrigin, @@ -209,5 +210,5 @@ export function createOktaProvider( }); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler(providerId, envProviders); } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 68b84c7e33..40c80de4e4 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -147,5 +147,5 @@ export function createSamlProvider( envProviders[env] = new SamlAuthProvider(opts); } - return new EnvironmentHandler(envProviders); + return new EnvironmentHandler('saml', envProviders); } diff --git a/plugins/proxy-backend/README.md b/plugins/proxy-backend/README.md index 315178dac6..d12031f495 100644 --- a/plugins/proxy-backend/README.md +++ b/plugins/proxy-backend/README.md @@ -14,13 +14,13 @@ To run it within the backend do: 1. Register the router in `packages/backend/src/index.ts`: -``` +```ts const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const service = createServiceBuilder(module) - .loadConfig(configReader) - /** several different routers */ - .addRouter('/', await proxy(proxyEnv)); + .loadConfig(configReader) + /** several different routers */ + .addRouter('/', await proxy(proxyEnv)); ``` 2. Start the backend @@ -33,5 +33,4 @@ This will launch the full example backend. ## Links -- (http-proxy-middleware)[https://www.npmjs.com/package/http-proxy-middleware] -- (The Backstage homepage)[https://backstage.io] +- [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware) diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml index 363d488448..71f75c7695 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -8,6 +8,7 @@ metadata: - Recommended - React spec: + owner: web@example.com templater: cookiecutter type: website path: '.' diff --git a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml index 19ec7935d9..0fefce37b9 100644 --- a/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/springboot-template/template.yaml @@ -8,6 +8,7 @@ metadata: - Recommended - Java spec: + owner: service@example.com templater: cookiecutter type: service path: '.' diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index eba7def9af..5fd64d69fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Publisher } from './types'; +import { PublisherBase } from './types'; import { Octokit } from '@octokit/rest'; import { JsonValue } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { Repository, Remote, Signature, Cred } from 'nodegit'; -export class GithubPublisher implements Publisher { +export class GithubPublisher implements PublisherBase { private client: Octokit; constructor({ client }: { client: Octokit }) { this.client = client; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index 17b357a4ef..5d4e9dd521 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -21,7 +21,7 @@ import { JsonValue } from '@backstage/config'; * Publisher is in charge of taking a folder created by * the templater, and pushing it to a remote storage */ -export type Publisher = { +export type PublisherBase = { /** * * @param opts object containing the template entity from the service diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 225662142f..fcfd5765a1 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -26,13 +26,13 @@ import { RequiredTemplateValues, StageContext, TemplaterBuilder, - Publisher, + PublisherBase, } from '../scaffolder'; export interface RouterOptions { preparers: PreparerBuilder; templaters: TemplaterBuilder; - publisher: Publisher; + publisher: PublisherBase; logger: Logger; dockerClient: Docker; @@ -55,18 +55,6 @@ export async function createRouter( const jobProcessor = new JobProcessor(); router - .get('/v1/job/:jobId/stage/:index/log', ({ params }, res) => { - const job = jobProcessor.get(params.jobId); - - if (!job) { - res.status(404).send({ error: 'job not found' }); - return; - } - - const { log } = job.stages[Number(params.index)] ?? { log: [] }; - - res.send(log.join('')); - }) .get('/v1/job/:jobId', ({ params }, res) => { const job = jobProcessor.get(params.jobId); @@ -126,7 +114,7 @@ export async function createRouter( { name: 'Publish template', handler: async (ctx: StageContext<{ resultDir: string }>) => { - ctx.logger.info('Should not store the template'); + ctx.logger.info('Will now store the template'); const { remoteUrl } = await publisher.publish({ entity: ctx.entity, values: ctx.values, diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index ac13634842..a0957a6544 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -23,17 +23,12 @@ import { Lifecycle, Page, pageTheme, + Progress, SupportButton, useApi, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog'; -import { - Button, - Grid, - LinearProgress, - Link, - Typography, -} from '@material-ui/core'; +import { Button, Grid } from '@material-ui/core'; import React, { useEffect } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import useStaleWhileRevalidate from 'swr'; @@ -95,14 +90,7 @@ export const ScaffolderPage: React.FC<{}> = () => { documentation, ...). - - NOTE! This feature is WIP. You can follow progress{' '} - - here - - . - - {!templates && isValidating && } + {!templates && isValidating && } {templates && templates.map(template => { diff --git a/yarn.lock b/yarn.lock index 81b1d95697..3b883964f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/code-frame@7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + "@babel/code-frame@7.5.5": version "7.5.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -5950,7 +5957,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -8114,6 +8121,17 @@ eslint-config-prettier@^6.0.0: dependencies: get-stdin "^6.0.0" +eslint-formatter-friendly@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/eslint-formatter-friendly/-/eslint-formatter-friendly-7.0.0.tgz#32a4998ababa0a39994aed629b831fda7dabc864" + integrity sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w== + dependencies: + "@babel/code-frame" "7.0.0" + chalk "2.4.2" + extend "3.0.2" + strip-ansi "5.2.0" + text-table "0.2.0" + eslint-import-resolver-node@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" @@ -8574,7 +8592,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@3.0.2, extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==