diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 35d354fefc..751ec4914d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,4 +4,6 @@
# The last matching pattern takes precedence.
# https://help.github.com/articles/about-codeowners/
-* @spotify/backstage-core
+* @spotify/backstage-core
+/plugins/techdocs @spotify/techdocs-core
+/packages/techdocs-cli @spotify/techdocs-core
diff --git a/.github/workflows/techdocs.yml b/.github/workflows/techdocs.yml
new file mode 100644
index 0000000000..6d6a6e904e
--- /dev/null
+++ b/.github/workflows/techdocs.yml
@@ -0,0 +1,35 @@
+name: TechDocs
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/techdocs.yml'
+ - 'packages/techdocs-cli/**'
+ - 'plugins/techdocs/**'
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ python-version: [3.7]
+
+ env:
+ TECHDOCS_CORE_PATH: ./plugins/techdocs/mkdocs/container/techdocs-core
+
+ name: Python ${{ matrix.node-version }} on ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v2
+
+ # Lint Python code for techdocs-core package
+ - name: prepare python environment
+ run: |
+ python3 -m pip install --index-url https://pypi.org/simple/ setuptools
+ python3 -m pip install --upgrade pip
+ python3 -m pip install --index-url https://pypi.org/simple/ -r $TECHDOCS_CORE_PATH/requirements.txt
+
+ - name: lint techdocs-core package
+ run: |
+ python3 -m black --check $TECHDOCS_CORE_PATH/src
diff --git a/.gitignore b/.gitignore
index f5794babbc..a1eb87e8d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -116,3 +116,6 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
+
+# Temporary change files created by Vim
+*.swp
diff --git a/ADOPTERS.md b/ADOPTERS.md
new file mode 100644
index 0000000000..d0f8c0a97b
--- /dev/null
+++ b/ADOPTERS.md
@@ -0,0 +1,3 @@
+| 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.|
diff --git a/app-config.yaml b/app-config.yaml
index 6ff336c727..39527c20bd 100644
--- a/app-config.yaml
+++ b/app-config.yaml
@@ -4,6 +4,11 @@ app:
backend:
baseUrl: http://localhost:7000
+ listen: 0.0.0.0:7000
+ cors:
+ origin: http://localhost:3000
+ methods: [GET, POST, PUT, DELETE]
+ credentials: true
organization:
name: Spotify
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 5167139d45..8c9febce07 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -97,17 +97,9 @@ app and configures which plugins are available to use in the app. The
**software engineer** uses the app's functionality and interacts with its
plugins.
-### What is a "plugin" in Backstage?
+### What is the use of a "plugin" in Backstage?
-By far, our most-used plugin is our TechDocs plugin, which we use for creating
-technical documentation. Our philosophy at Spotify is to treat "docs like code",
-where you write documentation using the same workflow as you write your code.
-This makes it easier to create, find, and update documentation. We hope to
-release
-[the open source version](https://github.com/spotify/backstage/issues/687) in
-the future. (See also:
-"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage)"
-above)
+ A Backstage Plugin adds functionality to Backstage.
### Do I have to write plugins in TypeScript?
diff --git a/docs/auth/README.md b/docs/auth/README.md
new file mode 100644
index 0000000000..081d10add6
--- /dev/null
+++ b/docs/auth/README.md
@@ -0,0 +1,86 @@
+# User Authentication and Authorization in Backstage
+
+## Summary
+
+The purpose of the Auth APIs in Backstage are to identify the user, and to
+provide a way for plugins to request access to 3rd party services on behalf of
+the user (OAuth). This documentation focuses on the implementation of that
+solution and how to extend it. For documentation on how to consume the Auth APIs
+in a plugin, see [TODO](#TODO).
+
+### Accessing Third Party Services
+
+The main pattern for talking to third party services in Backstage is
+user-to-server requests, where short-lived OAuth Access Tokens are requested by
+plugins to authenticate calls to external services. These calls can be made
+either directly to the services or through a backend plugin or service.
+
+By relying on user-to-server calls we keep the coupling between the frontend and
+backend low, and provide a much lower barrier for plugins to make use of third
+party services. This is in comparison to for example a session-based system,
+where access tokens are stored server-side. Such a solution would require a much
+deeper coupling between the auth backend plugin, its session storage, and other
+backend plugins or separate services. A goal of Backstage is to make it as easy
+as possible to create new plugins, and an auth solution based on user-to-server
+OAuth helps in that regard.
+
+The method with which frontend plugins request access to third party services is
+through [Utility APIs](../getting-started/utility-apis.md) for each service
+provider. For a full list of providers, see [TODO](#TODO).
+
+### Identity - WIP
+
+Identity management is still work in progress, but there are already a couple of
+pieces in place that can be used.
+
+#### Identity for Plugin Developers
+
+As a plugin developer, there are two main touchpoints for identities: the
+`IdentityApi` exported by `@backstage/core` via the `identityApiRef`, and a not
+yet existing middleware exported by `@backstage/backend-common`.
+
+The `IdentityApi` gives access to the signed-in user's identity in the frontend.
+It provides access to the user's ID, lightweight profile information, and an ID
+token used to make authenticated calls within Backstage.
+
+The middleware that will be provided by `@backstage/backend-common` allows
+verification of Backstage ID tokens, and optionally loading additional
+information about the user. The progress is tracked in
+https://github.com/spotify/backstage/issues/1435.
+
+#### Identity for App Developers
+
+If you're setting up your own Backstage app, or want to add a new identity
+provider, there are three touchpoints: the frontend auth APIs in
+`@backstage/core-api`, the backend auth providers in `auth-backend`, and the
+`SignInPage` component configured in the Backstage app via `createApp`.
+
+The frontend APIs and backend providers are tightly coupled together for each
+auth provider, and together they implement an e2e auth flow. Only some auth
+providers also act as identity providers though. For example, at the moment of
+writing, the Google Auth provider is able to act as a Backstage identity
+provider, but the GitHub one can not. For an auth provider to also act as an
+identity provider, it needs to implement the `BackstageIdentityApi` in the
+frontend, and in the backend it needs to return a `BackstageIdentity` structure.
+
+It is up to each provider to implement the mapping between a provider identity
+and the corresponding Backstage identity. That is currently still work in
+progress, and as a stop-gap for example the Google provider returns the local
+part of the user's email as the user ID.
+
+The final piece of the puzzle is the `SignInPage` component that can be
+configured as part of the app. Without a sign-in page, Backstage will fall back
+to a `guest` identity for all users, without any ID token. To enable sign-in, a
+`SignInPage` needs to be configured, which in turn has to supply a user to the
+app. The `@backstage/core` package provides a basic sign-in page that allows
+both the user and the app developer to choose between a couple of different
+sign-in methods.
+
+## Further Reading
+
+More details are provided in dedicated sections of the documentation.
+
+- [OAuth](./oauth): Description of the generic OAuth flow implemented by the
+ [auth-backend](../../plugins/auth-backend).
+- [Glossary](./glossary): Glossary of some common terms related to the auth
+ flows.
diff --git a/docs/auth/overview.md b/docs/auth/overview.md
deleted file mode 100644
index 2806b2dccf..0000000000
--- a/docs/auth/overview.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# User Authentication and Authorization in Backstage
-
-## Summary
-
-The purpose of the Auth APIs in Backstage are to identify the user, and to
-provide a way for plugins to request access to 3rd party services on behalf of
-the user (OAuth). This documentation focuses on the implementation of that
-solution and how to extend it. For documentation on how to consume the Auth APIs
-in a plugin, see [TODO](#TODO).
-
-### Accessing Third Party Services
-
-The main pattern for talking to third party services in Backstage is
-user-to-server requests, where short-lived OAuth Access Tokens are requested by
-plugins to authenticate calls to external services. These calls can be made
-either directly to the services or through a backend plugin or service.
-
-By relying on user-to-server calls we keep the coupling between the frontend and
-backend low, and provide a much lower barrier for plugins to make use of third
-party services. This is in comparison to for example a session-based system,
-where access tokens are stored server-side. Such a solution would require a much
-deeper coupling between the auth backend plugin, its session storage, and other
-backend plugins or separate services. A goal of Backstage is to make it as easy
-as possible to create new plugins, and an auth solution based on user-to-server
-OAuth helps in that regard.
-
-The method with which frontend plugins request access to third party services is
-through [Utility APIs](../getting-started/utility-apis.md) for each service
-provider. For a full list of providers, see [TODO](#TODO).
-
-### Identity - TODO
-
-This documentation currently only covers the OAuth use-case, as identity
-management is not settled yet and part of an
-[upcoming milestone](https://github.com/spotify/backstage/milestone/12).
-
-## Further Reading
-
-More details are provided in dedicated sections of the documentation.
-
-- [OAuth](./oauth): Description of the generic OAuth flow implemented by the
- [auth-backend](../../plugins/auth-backend).
-- [Glossary](./glossary): Glossary of some common terms related to the auth
- flows.
diff --git a/lerna.json b/lerna.json
index 5d63f88c11..9e00bb25a6 100644
--- a/lerna.json
+++ b/lerna.json
@@ -2,5 +2,5 @@
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"useWorkspaces": true,
- "version": "0.1.1-alpha.9"
+ "version": "0.1.1-alpha.12"
}
diff --git a/packages/app/package.json b/packages/app/package.json
index 6aa82a0b36..118b4c15ff 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,39 +1,44 @@
{
"name": "example-app",
- "version": "0.1.1-alpha.9",
+ "version": "0.1.1-alpha.12",
"private": true,
"dependencies": {
- "@backstage/cli": "^0.1.1-alpha.9",
- "@backstage/core": "^0.1.1-alpha.9",
- "@backstage/plugin-catalog": "^0.1.1-alpha.9",
- "@backstage/plugin-circleci": "^0.1.1-alpha.9",
- "@backstage/plugin-explore": "^0.1.1-alpha.9",
- "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.9",
- "@backstage/plugin-lighthouse": "^0.1.1-alpha.9",
- "@backstage/plugin-register-component": "^0.1.1-alpha.9",
- "@backstage/plugin-scaffolder": "^0.1.1-alpha.9",
- "@backstage/plugin-sentry": "^0.1.1-alpha.9",
- "@backstage/plugin-tech-radar": "^0.1.1-alpha.9",
- "@backstage/plugin-welcome": "^0.1.1-alpha.9",
- "@backstage/theme": "^0.1.1-alpha.9",
+ "@backstage/cli": "^0.1.1-alpha.12",
+ "@backstage/core": "^0.1.1-alpha.12",
+ "@backstage/plugin-catalog": "^0.1.1-alpha.12",
+ "@backstage/plugin-circleci": "^0.1.1-alpha.12",
+ "@backstage/plugin-explore": "^0.1.1-alpha.12",
+ "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.12",
+ "@backstage/plugin-graphiql": "^0.1.1-alpha.12",
+ "@backstage/plugin-lighthouse": "^0.1.1-alpha.12",
+ "@backstage/plugin-register-component": "^0.1.1-alpha.12",
+ "@backstage/plugin-scaffolder": "^0.1.1-alpha.12",
+ "@backstage/plugin-sentry": "^0.1.1-alpha.12",
+ "@backstage/plugin-tech-radar": "^0.1.1-alpha.12",
+ "@backstage/plugin-techdocs": "^0.1.1-alpha.12",
+ "@backstage/plugin-welcome": "^0.1.1-alpha.12",
+ "@backstage/test-utils": "^0.1.1-alpha.12",
+ "@backstage/theme": "^0.1.1-alpha.12",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.12.21",
+ "react-router": "6.0.0-alpha.5",
"react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0",
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@testing-library/cypress": "^6.0.0",
- "@testing-library/jest-dom": "^5.7.0",
- "@testing-library/react": "^9.3.2",
- "@testing-library/user-event": "^10.2.4",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/jquery": "^3.3.34",
"@types/node": "^12.0.0",
+ "@types/react-dom": "^16.9.8",
"@types/zen-observable": "^0.8.0",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx
index 79f78e45e0..65e8ff64e6 100644
--- a/packages/app/src/App.test.tsx
+++ b/packages/app/src/App.test.tsx
@@ -15,23 +15,24 @@
*/
import React from 'react';
-import { render } from '@testing-library/react';
+import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
describe('App', () => {
- beforeAll(() => {
- Object.defineProperty(window, 'matchMedia', {
- value: jest.fn(() => {
- return {
- matches: true,
- addListener: jest.fn(),
- removeListener: jest.fn(),
- };
- }),
+ it('should render', async () => {
+ Object.defineProperty(process.env, 'APP_CONFIG', {
+ configurable: true,
+ value: [
+ {
+ data: {
+ app: { title: 'Test' },
+ },
+ context: 'test',
+ },
+ ],
});
- });
- it('should render', () => {
- const rendered = render();
+
+ const rendered = await renderWithEffects();
expect(rendered.baseElement).toBeInTheDocument();
});
});
diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts
index 9bfc618216..5011b16eaf 100644
--- a/packages/app/src/apis.ts
+++ b/packages/app/src/apis.ts
@@ -45,6 +45,10 @@ import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci';
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
import { gitOpsApiRef, GitOpsRestApi } from '@backstage/plugin-gitops-profiles';
+import {
+ graphQlBrowseApiRef,
+ GraphQLEndpoints,
+} from '@backstage/plugin-graphiql';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
@@ -78,7 +82,7 @@ export const apis = (config: ConfigApi) => {
}),
);
- builder.add(
+ const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7000',
@@ -105,5 +109,22 @@ export const apis = (config: ConfigApi) => {
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
+ builder.add(
+ graphQlBrowseApiRef,
+ GraphQLEndpoints.from([
+ GraphQLEndpoints.create({
+ id: 'gitlab',
+ title: 'GitLab',
+ url: 'https://gitlab.com/api/graphql',
+ }),
+ GraphQLEndpoints.github({
+ id: 'github',
+ title: 'GitHub',
+ errorApi,
+ githubAuthApi,
+ }),
+ ]),
+ );
+
return builder.build();
};
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 453c98bc72..4b92f8281e 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -39,6 +39,7 @@ import {
SidebarPinButton,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
+import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -94,6 +95,11 @@ const Root: FC<{}> = ({ children }) => (
+
diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx
index 2ea8d3f1dd..a38159a258 100644
--- a/packages/app/src/index.tsx
+++ b/packages/app/src/index.tsx
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+// eslint-disable-next-line monorepo/no-internal-import
+import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts
index a6485835f7..09cd117fc0 100644
--- a/packages/app/src/plugins.ts
+++ b/packages/app/src/plugins.ts
@@ -23,3 +23,5 @@ export { plugin as Circleci } from '@backstage/plugin-circleci';
export { plugin as RegisterComponent } from '@backstage/plugin-register-component';
export { plugin as Sentry } from '@backstage/plugin-sentry';
export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles';
+export { plugin as TechDocs } from '@backstage/plugin-techdocs';
+export { plugin as GraphiQL } from '@backstage/plugin-graphiql';
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 1a52e06013..b0aef88fea 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.1.1-alpha.9",
+ "version": "0.1.1-alpha.12",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -29,6 +29,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
+ "@backstage/config": "^0.1.1-alpha.12",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"compression": "^1.7.4",
@@ -40,7 +41,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.9",
+ "@backstage/cli": "^0.1.1-alpha.12",
"@types/compression": "^1.7.0",
"@types/http-errors": "^1.6.3",
"@types/morgan": "^1.9.0",
diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts
index 3d4252b378..c725096c75 100644
--- a/packages/backend-common/src/hot.ts
+++ b/packages/backend-common/src/hot.ts
@@ -14,10 +14,38 @@
* limitations under the License.
*/
+// Find all active hot module APIs of all ancestors of a module, including the module itself
+function findAllAncestors(_module: NodeModule): NodeModule[] {
+ const ancestors = new Array();
+ const parentIds = new Set();
+
+ function add(id: string | number, m: NodeModule) {
+ if (parentIds.has(id)) {
+ return;
+ }
+ parentIds.add(id);
+ ancestors.push(m);
+
+ for (const parentId of (m as any).parents) {
+ const parent = require.cache[parentId];
+ if (parent) {
+ add(parentId, parent);
+ }
+ }
+ }
+
+ add(_module.id, _module);
+
+ return ancestors;
+}
+
/**
- * This function allows devs to cleanup
- * ongoing effects when module gets hot-reloaded
+ * useHotCleanup allows cleanup of ongoing effects when a module is
+ * hot-reloaded during development. The cleanup function will be called
+ * whenever the module itself or any of its parent modules is hot-reloaded.
+ *
* Useful for cleaning intervals, timers, requests etc
+ *
* @example
* ```ts
* const intervalId = setInterval(doStuff, 1000);
@@ -28,69 +56,69 @@
*/
export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) {
if (_module.hot) {
- _module.hot.addDisposeHandler(() => {
- cancelEffect();
- });
+ const ancestors = findAllAncestors(_module);
+ let cancelled = false;
+
+ const handler = () => {
+ if (!cancelled) {
+ cancelled = true;
+ cancelEffect();
+ }
+ };
+
+ for (const m of ancestors) {
+ m.hot?.addDisposeHandler(handler);
+ }
}
}
+const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key';
+
/**
- * This function allows devs to preserve
- * some value between hot-reloads.
- * Useful for stateful parts of the backend
+ * Memoizes a generated value across hot-module reloads. This is useful for
+ * stateful parts of the backend, e.g. to retain a database.
+ *
* @example
* ```ts
* const db = useHotMemoize(module, () => createDB(dbParams));
* ```
- * @param _module Reference to the current module where you invoke the fn
- * @param valueFactory Fn that returns the value you want to memoize
+ *
* @warning Don't use inside conditionals or loops,
* same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html)
+ *
+ * @param _module Reference to the current module where you invoke the fn
+ * @param valueFactory Fn that returns the value you want to memoize
*/
export function useHotMemoize(
_module: NodeModule,
valueFactory: () => T,
): T {
- const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key';
-
if (!_module.hot) {
- // Just return value straight away
return valueFactory();
}
- if (_module.hot && typeof _module.hot.data === 'undefined') {
- // First run, init the module data
+ // When starting blank, reset the counter
+ if (!_module.hot.data?.[CURRENT_HOT_MEMOIZE_INDEX_KEY]) {
+ for (const ancestor of findAllAncestors(_module)) {
+ ancestor.hot?.addDisposeHandler(data => {
+ data[CURRENT_HOT_MEMOIZE_INDEX_KEY] = 1;
+ });
+ }
+
_module.hot.data = {
- [CURRENT_HOT_MEMOIZE_INDEX_KEY]: 0,
+ ..._module.hot.data,
+ [CURRENT_HOT_MEMOIZE_INDEX_KEY]: 1,
};
}
- // Let's store data per module based on the order of the code invocation
- const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY];
- // Increasing the counter after each call
- _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] += 1;
+ // Store data per module, based on the order of the code invocation
+ const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY]++;
+ const value = _module.hot.data[index] ?? valueFactory();
- const prevValue = _module.hot.data[index];
- const createDisposeHandler = (value: any) => (data: {
- [key: number]: any;
- [indexKey: string]: number;
- }) => {
- // Preserving the value through the HMR process
+ // Always add a handler that, upon a HMR event, reinstates the value.
+ _module.hot.addDisposeHandler(data => {
data[index] = value;
- // Decreasing the counter after each handler
- data[CURRENT_HOT_MEMOIZE_INDEX_KEY] =
- // First hot update is still different, need to populate the data
- typeof data[CURRENT_HOT_MEMOIZE_INDEX_KEY] === 'undefined'
- ? _module.hot!.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1
- : data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1;
- };
+ });
- if (prevValue) {
- _module.hot!.addDisposeHandler(createDisposeHandler(prevValue));
- return prevValue;
- }
-
- const newValue = valueFactory();
- _module.hot.addDisposeHandler(createDisposeHandler(newValue));
- return newValue;
+ return value;
}
diff --git a/packages/backend-common/src/service/createServiceBuilder.ts b/packages/backend-common/src/service/createServiceBuilder.ts
index daef612fcf..c62921afc3 100644
--- a/packages/backend-common/src/service/createServiceBuilder.ts
+++ b/packages/backend-common/src/service/createServiceBuilder.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ServiceBuilderImpl } from './ServiceBuilderImpl';
+import { ServiceBuilderImpl } from './lib/ServiceBuilderImpl';
/**
* Creates a new service builder.
diff --git a/packages/backend-common/src/service/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
similarity index 66%
rename from packages/backend-common/src/service/ServiceBuilderImpl.ts
rename to packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
index 931d29b841..19418b2671 100644
--- a/packages/backend-common/src/service/ServiceBuilderImpl.ts
+++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import { ConfigReader } from '@backstage/config';
import compression from 'compression';
import cors from 'cors';
import express, { Router } from 'express';
@@ -21,37 +22,66 @@ import helmet from 'helmet';
import { Server } from 'http';
import stoppable from 'stoppable';
import { Logger } from 'winston';
-import { getRootLogger } from '../logging';
+import { useHotCleanup } from '../../hot';
+import { getRootLogger } from '../../logging';
import {
errorHandler,
notFoundHandler,
requestLoggingHandler,
-} from '../middleware';
-import { ServiceBuilder } from './types';
-import { useHotCleanup } from '../hot';
+} from '../../middleware';
+import { ServiceBuilder } from '../types';
+import { readBaseOptions, readCorsOptions } from './config';
const DEFAULT_PORT = 7000;
+const DEFAULT_HOST = 'localhost';
export class ServiceBuilderImpl implements ServiceBuilder {
private port: number | undefined;
+ private host: string | undefined;
private logger: Logger | undefined;
private corsOptions: cors.CorsOptions | undefined;
private routers: [string, Router][];
- /**
- * Reference to the module where builder is created
- * Needed for the HMR
- */
+ // Reference to the module where builder is created - needed for hot module
+ // reloading
private module: NodeModule;
+
constructor(module: NodeModule) {
this.routers = [];
this.module = module;
}
+ loadConfig(config: ConfigReader): ServiceBuilder {
+ const backendConfig = config.getOptionalConfig('backend');
+ if (!backendConfig) {
+ return this;
+ }
+
+ const baseOptions = readBaseOptions(backendConfig);
+ if (baseOptions.listenPort) {
+ this.port = baseOptions.listenPort;
+ }
+ if (baseOptions.listenHost) {
+ this.host = baseOptions.listenHost;
+ }
+
+ const corsOptions = readCorsOptions(backendConfig);
+ if (corsOptions) {
+ this.corsOptions = corsOptions;
+ }
+
+ return this;
+ }
+
setPort(port: number): ServiceBuilder {
this.port = port;
return this;
}
+ setHost(host: string): ServiceBuilder {
+ this.host = host;
+ return this;
+ }
+
setLogger(logger: Logger): ServiceBuilder {
this.logger = logger;
return this;
@@ -69,7 +99,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
start(): Promise {
const app = express();
- const { port, logger, corsOptions } = this.getOptions();
+ const { port, host, logger, corsOptions } = this.getOptions();
app.use(helmet());
if (corsOptions) {
@@ -89,9 +119,10 @@ export class ServiceBuilderImpl implements ServiceBuilder {
logger.error(`Failed to start up on port ${port}, ${e}`);
reject(e);
});
+
const server = stoppable(
- app.listen(port, () => {
- logger.info(`Listening on port ${port}`);
+ app.listen(port, host, () => {
+ logger.info(`Listening on ${host}:${port}`);
}),
0,
);
@@ -108,26 +139,14 @@ export class ServiceBuilderImpl implements ServiceBuilder {
private getOptions(): {
port: number;
+ host: string;
logger: Logger;
corsOptions?: cors.CorsOptions;
} {
- let port: number;
- if (this.port !== undefined) {
- port = this.port;
- } else {
- port = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
- }
-
- let logger: Logger;
- if (this.logger) {
- logger = this.logger;
- } else {
- logger = getRootLogger();
- }
-
return {
- port,
- logger,
+ port: this.port ?? DEFAULT_PORT,
+ host: this.host ?? DEFAULT_HOST,
+ logger: this.logger ?? getRootLogger(),
corsOptions: this.corsOptions,
};
}
diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts
new file mode 100644
index 0000000000..064eacc4f2
--- /dev/null
+++ b/packages/backend-common/src/service/lib/config.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ConfigReader } from '@backstage/config';
+import { CorsOptions } from 'cors';
+
+export type BaseOptions = {
+ listenPort?: number;
+ listenHost?: string;
+};
+
+/**
+ * Reads some base options out of a config object.
+ *
+ * @param config The root of a backend config object
+ * @returns A base options object
+ *
+ * @example
+ * ```json
+ * {
+ * baseUrl: "http://localhost:7000",
+ * listen: "0.0.0.0:7000"
+ * }
+ * ```
+ */
+export function readBaseOptions(config: ConfigReader): BaseOptions {
+ // TODO(freben): Expand this to support more addresses and perhaps optional
+ const { host, port } = parseListenAddress(config.getString('listen'));
+ return removeUnknown({
+ listenPort: port,
+ listenHost: host,
+ });
+}
+
+/**
+ * Attempts to read a CORS options object from the root of a config object.
+ *
+ * @param config The root of a backend config object
+ * @returns A CORS options object, or undefined if not specified
+ *
+ * @example
+ * ```json
+ * {
+ * cors: {
+ * origin: "http://localhost:3000",
+ * credentials: true
+ * }
+ * }
+ * ```
+ */
+export function readCorsOptions(config: ConfigReader): CorsOptions | undefined {
+ const cc = config.getOptionalConfig('cors');
+ if (!cc) {
+ return undefined;
+ }
+
+ return removeUnknown({
+ origin: getOptionalStringOrStrings(cc, 'origin'),
+ methods: getOptionalStringOrStrings(cc, 'methods'),
+ allowedHeaders: getOptionalStringOrStrings(cc, 'allowedHeaders'),
+ exposedHeaders: getOptionalStringOrStrings(cc, 'exposedHeaders'),
+ credentials: cc.getOptionalBoolean('credentials'),
+ maxAge: cc.getOptionalNumber('maxAge'),
+ preflightContinue: cc.getOptionalBoolean('preflightContinue'),
+ optionsSuccessStatus: cc.getOptionalNumber('optionsSuccessStatus'),
+ });
+}
+
+function getOptionalStringOrStrings(
+ config: ConfigReader,
+ key: string,
+): string | string[] | undefined {
+ const value = config.getOptional(key);
+ if (
+ value === undefined ||
+ typeof value === 'string' ||
+ isStringArray(value)
+ ) {
+ return value;
+ }
+ throw new Error(`Expected string or array of strings, got ${typeof value}`);
+}
+
+function isStringArray(value: any): value is string[] {
+ if (!Array.isArray(value)) {
+ return false;
+ }
+ for (const v of value) {
+ if (typeof v !== 'string') {
+ return false;
+ }
+ }
+ return true;
+}
+
+function removeUnknown(obj: T): T {
+ return Object.fromEntries(
+ Object.entries(obj).filter(([, v]) => v !== undefined),
+ ) as T;
+}
+
+function parseListenAddress(value: string): { host?: string; port?: number } {
+ const parts = value.split(':');
+ if (parts.length === 1) {
+ return { port: parseInt(parts[0], 10) };
+ }
+ if (parts.length === 2) {
+ return { host: parts[0], port: parseInt(parts[1], 10) };
+ }
+ throw new Error(
+ `Unable to parse listen address ${value}, expected or :`,
+ );
+}
diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts
index 306f0d587e..b39df27527 100644
--- a/packages/backend-common/src/service/types.ts
+++ b/packages/backend-common/src/service/types.ts
@@ -14,12 +14,20 @@
* limitations under the License.
*/
+import { ConfigReader } from '@backstage/config';
import cors from 'cors';
import { Router } from 'express';
import { Server } from 'http';
import { Logger } from 'winston';
export type ServiceBuilder = {
+ /**
+ * Sets the service parameters based on configuration.
+ *
+ * @param config The configuration to read
+ */
+ loadConfig(config: ConfigReader): ServiceBuilder;
+
/**
* Sets the port to listen on.
*
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 516208bf3b..ec54da358c 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.1.1-alpha.9",
+ "version": "0.1.1-alpha.12",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
@@ -17,22 +17,22 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
- "@backstage/backend-common": "^0.1.1-alpha.9",
- "@backstage/catalog-model": "^0.1.1-alpha.9",
- "@backstage/config": "^0.1.1-alpha.9",
- "@backstage/config-loader": "^0.1.1-alpha.9",
- "@backstage/plugin-auth-backend": "^0.1.1-alpha.9",
- "@backstage/plugin-catalog-backend": "^0.1.1-alpha.9",
- "@backstage/plugin-identity-backend": "^0.1.1-alpha.9",
- "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.9",
- "@backstage/plugin-sentry-backend": "^0.1.1-alpha.9",
+ "@backstage/backend-common": "^0.1.1-alpha.12",
+ "@backstage/catalog-model": "^0.1.1-alpha.12",
+ "@backstage/config": "^0.1.1-alpha.12",
+ "@backstage/config-loader": "^0.1.1-alpha.12",
+ "@backstage/plugin-auth-backend": "^0.1.1-alpha.12",
+ "@backstage/plugin-catalog-backend": "^0.1.1-alpha.12",
+ "@backstage/plugin-identity-backend": "^0.1.1-alpha.12",
+ "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.12",
+ "@backstage/plugin-sentry-backend": "^0.1.1-alpha.12",
"express": "^4.17.1",
"knex": "^0.21.1",
"sqlite3": "^4.2.0",
"winston": "^3.2.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.9",
+ "@backstage/cli": "^0.1.1-alpha.12",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/helmet": "^0.0.47"
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index df0d08fa7d..3246c5f6f5 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -55,7 +55,9 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
}
async function main() {
- const createEnv = makeCreateEnv(await loadConfig());
+ const configs = await loadConfig();
+ const configReader = ConfigReader.fromConfigs(configs);
+ const createEnv = makeCreateEnv(configs);
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
@@ -63,10 +65,7 @@ async function main() {
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
const service = createServiceBuilder(module)
- .enableCors({
- origin: 'http://localhost:3000',
- credentials: true,
- })
+ .loadConfig(configReader)
.addRouter('/catalog', await catalog(catalogEnv))
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
.addRouter(
diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts
index a9c687cbc4..b24dd6ca6b 100644
--- a/packages/backend/src/plugins/auth.ts
+++ b/packages/backend/src/plugins/auth.ts
@@ -19,7 +19,8 @@ import { PluginEnvironment } from '../types';
export default async function createPlugin({
logger,
+ database,
config,
}: PluginEnvironment) {
- return await createRouter({ logger, config });
+ return await createRouter({ logger, config, database });
}
diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts
index 08e700bc74..95d28eacb1 100644
--- a/packages/backend/src/plugins/scaffolder.ts
+++ b/packages/backend/src/plugins/scaffolder.ts
@@ -17,13 +17,20 @@
import {
CookieCutter,
createRouter,
- DiskStorage,
+ FilePreparer,
+ GithubPreparer,
+ Preparers,
} from '@backstage/plugin-scaffolder-backend';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({ logger }: PluginEnvironment) {
- const storage = new DiskStorage({ logger });
const templater = new CookieCutter();
+ const filePreparer = new FilePreparer();
+ const githubPreparer = new GithubPreparer();
+ const preparers = new Preparers();
- return await createRouter({ storage, templater, logger });
+ preparers.register('file', filePreparer);
+ preparers.register('github', githubPreparer);
+
+ return await createRouter({ preparers, templater, logger });
}
diff --git a/plugins/catalog-backend/examples/artist-lookup-component.yaml b/packages/catalog-model/examples/artist-lookup-component.yaml
similarity index 84%
rename from plugins/catalog-backend/examples/artist-lookup-component.yaml
rename to packages/catalog-model/examples/artist-lookup-component.yaml
index bb7d8d5767..448dd13fa3 100644
--- a/plugins/catalog-backend/examples/artist-lookup-component.yaml
+++ b/packages/catalog-model/examples/artist-lookup-component.yaml
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
- owner: tools@example.com
+ owner: artists@example.com
diff --git a/packages/catalog-model/examples/playback-lib-component.yaml b/packages/catalog-model/examples/playback-lib-component.yaml
new file mode 100644
index 0000000000..c32332d4d2
--- /dev/null
+++ b/packages/catalog-model/examples/playback-lib-component.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: playback-sdk
+ description: Audio and video playback SDK
+spec:
+ type: library
+ lifecycle: experimental
+ owner: players@example.com
diff --git a/plugins/catalog-backend/examples/playback-order-component.yaml b/packages/catalog-model/examples/playback-order-component.yaml
similarity index 85%
rename from plugins/catalog-backend/examples/playback-order-component.yaml
rename to packages/catalog-model/examples/playback-order-component.yaml
index fa9f68e77a..4f93b65edf 100644
--- a/plugins/catalog-backend/examples/playback-order-component.yaml
+++ b/packages/catalog-model/examples/playback-order-component.yaml
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
- owner: tools@example.com
+ owner: guest
diff --git a/plugins/catalog-backend/examples/podcast-api-component.yaml b/packages/catalog-model/examples/podcast-api-component.yaml
similarity index 84%
rename from plugins/catalog-backend/examples/podcast-api-component.yaml
rename to packages/catalog-model/examples/podcast-api-component.yaml
index 2a27a0a2c5..19f9a3d4de 100644
--- a/plugins/catalog-backend/examples/podcast-api-component.yaml
+++ b/packages/catalog-model/examples/podcast-api-component.yaml
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
- owner: tools@example.com
+ owner: players@example.com
diff --git a/plugins/catalog-backend/examples/queue-proxy-component.yaml b/packages/catalog-model/examples/queue-proxy-component.yaml
similarity index 100%
rename from plugins/catalog-backend/examples/queue-proxy-component.yaml
rename to packages/catalog-model/examples/queue-proxy-component.yaml
diff --git a/plugins/catalog-backend/examples/searcher-component.yaml b/packages/catalog-model/examples/searcher-component.yaml
similarity index 84%
rename from plugins/catalog-backend/examples/searcher-component.yaml
rename to packages/catalog-model/examples/searcher-component.yaml
index d82f6e42c7..33d765117c 100644
--- a/plugins/catalog-backend/examples/searcher-component.yaml
+++ b/packages/catalog-model/examples/searcher-component.yaml
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
- owner: tools@example.com
+ owner: guest
diff --git a/plugins/catalog-backend/examples/shuffle-api-component.yaml b/packages/catalog-model/examples/shuffle-api-component.yaml
similarity index 85%
rename from plugins/catalog-backend/examples/shuffle-api-component.yaml
rename to packages/catalog-model/examples/shuffle-api-component.yaml
index ed0517835c..275c48d6ba 100644
--- a/plugins/catalog-backend/examples/shuffle-api-component.yaml
+++ b/packages/catalog-model/examples/shuffle-api-component.yaml
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
- owner: tools@example.com
+ owner: guest
diff --git a/packages/catalog-model/examples/www-artist-component.yaml b/packages/catalog-model/examples/www-artist-component.yaml
new file mode 100644
index 0000000000..84379c93fc
--- /dev/null
+++ b/packages/catalog-model/examples/www-artist-component.yaml
@@ -0,0 +1,9 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: www-artist
+ description: Artist main website
+spec:
+ type: website
+ lifecycle: production
+ owner: artists@example.com
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index 2516263e8a..889d9d2e02 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/catalog-model",
- "version": "0.1.1-alpha.9",
+ "version": "0.1.1-alpha.12",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,20 +20,20 @@
"clean": "backstage-cli clean"
},
"dependencies": {
- "@backstage/config": "^0.1.1-alpha.9",
+ "@backstage/config": "^0.1.1-alpha.12",
"@types/yup": "^0.28.2",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
- "yup": "^0.28.5"
+ "yup": "^0.29.1"
},
"devDependencies": {
- "@backstage/cli": "^0.1.1-alpha.9",
+ "@backstage/cli": "^0.1.1-alpha.12",
"@types/express": "^4.17.6",
"@types/jest": "^25.2.2",
"@types/lodash": "^4.14.151",
"yaml": "^1.9.2"
},
"files": [
- "dist/**/*.{js,d.ts}"
+ "dist"
]
}
diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts
index 090c892294..0bef220a63 100644
--- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts
+++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts
@@ -26,6 +26,7 @@ export interface TemplateEntityV1alpha1 extends Entity {
kind: typeof KIND;
spec: {
type: string;
+ path?: string;
};
}
@@ -39,6 +40,7 @@ export class TemplateEntityV1alpha1Policy implements EntityPolicy {
spec: yup
.object({
type: yup.string().required().min(1),
+ path: yup.string(),
})
.required(),
});
diff --git a/packages/cli/src/commands/plugin/assets.d.ts b/packages/cli/asset-types/asset-types.d.ts
similarity index 83%
rename from packages/cli/src/commands/plugin/assets.d.ts
rename to packages/cli/asset-types/asset-types.d.ts
index 34bb6acb70..1763d256f5 100644
--- a/packages/cli/src/commands/plugin/assets.d.ts
+++ b/packages/cli/asset-types/asset-types.d.ts
@@ -14,16 +14,12 @@
* limitations under the License.
*/
+/* eslint-disable import/no-extraneous-dependencies */
+
///
///
///
-declare namespace NodeJS {
- interface ProcessEnv {
- readonly NODE_ENV: 'development' | 'production' | 'test';
- }
-}
-
declare module '*.bmp' {
const src: string;
export default src;
@@ -54,13 +50,15 @@ declare module '*.webp' {
export default src;
}
+declare module '*.icon.svg' {
+ import { ComponentType } from 'react';
+ import { SvgIconProps } from '@material-ui/core';
+
+ const Icon: ComponentType;
+ export default Icon;
+}
+
declare module '*.svg' {
- import * as React from 'react';
-
- export const ReactComponent: React.FunctionComponent & { title?: string }>;
-
const src: string;
export default src;
}
@@ -94,7 +92,3 @@ declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}
-
-declare module 'rollup-plugin-image-files' {
- export default function image(): any;
-}
diff --git a/packages/cli/asset-types/asset-types.js b/packages/cli/asset-types/asset-types.js
new file mode 100644
index 0000000000..65ef751860
--- /dev/null
+++ b/packages/cli/asset-types/asset-types.js
@@ -0,0 +1,2 @@
+// NOOP, this is just here to unbreak module resolution
+// eslint-disable-next-line notice/notice
diff --git a/packages/cli/asset-types/package.json b/packages/cli/asset-types/package.json
new file mode 100644
index 0000000000..ffa5e09fc1
--- /dev/null
+++ b/packages/cli/asset-types/package.json
@@ -0,0 +1,5 @@
+{
+ "main": "asset-types.js",
+ "types": "asset-types.d.ts",
+ "description": "Provides types for asset files that are handled by the Backstage CLI"
+}
diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js
index ad24976ad0..80e0bcf26c 100644
--- a/packages/cli/config/jest.js
+++ b/packages/cli/config/jest.js
@@ -38,11 +38,16 @@ async function getConfig() {
transform: {
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
'\\.(js|jsx|ts|tsx)': require.resolve('ts-jest'),
+ '\\.(bmp|gif|jpe|png|frag|xml|svg)': require.resolve(
+ './jestFileTransform.js',
+ ),
},
// Default behaviour is to not apply transforms for node_modules, but we still want
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
- transformIgnorePatterns: ['/node_modules/(?!.*\\.esm\\.js$)'],
+ transformIgnorePatterns: [
+ '/node_modules/(?!.*\\.(?:esm\\.js|bmp|gif|jpe|png|frag|xml|svg)$)',
+ ],
};
// Use src/setupTests.ts as the default location for configuring test env
diff --git a/packages/cli/config/jestFileTransform.js b/packages/cli/config/jestFileTransform.js
new file mode 100644
index 0000000000..e6ff1895f8
--- /dev/null
+++ b/packages/cli/config/jestFileTransform.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const path = require('path');
+
+module.exports = {
+ process(src, filename) {
+ const assetFilename = JSON.stringify(path.basename(filename));
+
+ if (filename.match(/\.icon\.svg$/)) {
+ return `const React = require('react');
+ const SvgIcon = require('@material-ui/core/SvgIcon').default;
+ module.exports = {
+ __esModule: true,
+ default: props => React.createElement(SvgIcon, props, {
+ $$typeof: Symbol.for('react.element'),
+ type: 'svg',
+ ref: ref,
+ key: null,
+ props: Object.assign({}, props, {
+ children: ${assetFilename}
+ })
+ })
+ };`;
+ }
+
+ return `module.exports = ${assetFilename};`;
+ },
+};
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 96ca8e4a4f..f74c1b1fb0 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.1.1-alpha.9",
+ "version": "0.1.1-alpha.12",
"private": false,
"publishConfig": {
"access": "public"
@@ -29,16 +29,20 @@
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
- "@backstage/config": "0.1.1-alpha.9",
- "@backstage/config-loader": "^0.1.1-alpha.9",
+ "@backstage/config": "^0.1.1-alpha.12",
+ "@backstage/config-loader": "^0.1.1-alpha.12",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
- "@rollup/plugin-commonjs": "^12.0.0",
+ "@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-json": "^4.0.2",
- "@rollup/plugin-node-resolve": "^7.1.1",
+ "@rollup/plugin-node-resolve": "^8.1.0",
"@spotify/eslint-config": "^7.0.1",
"@sucrase/webpack-loader": "^2.0.0",
+ "@svgr/plugin-jsx": "4.3.x",
+ "@svgr/plugin-svgo": "4.3.x",
+ "@svgr/rollup": "4.3.x",
+ "@svgr/webpack": "4.3.x",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^1.7.1",
@@ -112,6 +116,7 @@
"zombie": "^6.1.4"
},
"files": [
+ "asset-types",
"templates",
"config",
"bin",
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts
index 6fe8092e50..aab244f7e9 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/lib/bundler/config.ts
@@ -178,8 +178,7 @@ export function createBackendConfig(
context: paths.targetPath,
entry: [
'webpack/hot/poll?100',
- paths.targetEntry,
- ...(paths.targetRunFile ? [paths.targetRunFile] : []),
+ paths.targetRunFile ? paths.targetRunFile : paths.targetEntry,
],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts
index 0cd800f2ea..5f881bd53c 100644
--- a/packages/cli/src/lib/bundler/transforms.ts
+++ b/packages/cli/src/lib/bundler/transforms.ts
@@ -17,6 +17,7 @@
import webpack, { Module, Plugin } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { BundlingOptions, BackendBundlingOptions } from './types';
+import { svgrTemplate } from '../svgrTemplate';
type Transforms = {
loaders: Module['rules'];
@@ -46,7 +47,28 @@ export const transforms = (
},
},
{
- test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.frag/, /\.xml/],
+ test: [/\.icon\.svg$/],
+ use: [
+ {
+ loader: require.resolve('@sucrase/webpack-loader'),
+ options: { transforms: ['jsx'] },
+ },
+ {
+ loader: require.resolve('@svgr/webpack'),
+ options: { babel: false, template: svgrTemplate },
+ },
+ ],
+ },
+ {
+ test: [
+ /\.bmp$/,
+ /\.gif$/,
+ /\.jpe?g$/,
+ /\.png$/,
+ /\.frag/,
+ { test: /\.svg/, not: [/\.icon\.svg/] },
+ /\.xml/,
+ ],
loader: require.resolve('url-loader'),
options: {
limit: 10000,
diff --git a/packages/cli/src/lib/packager/config.ts b/packages/cli/src/lib/packager/config.ts
index 30680ef3b9..5987290604 100644
--- a/packages/cli/src/lib/packager/config.ts
+++ b/packages/cli/src/lib/packager/config.ts
@@ -23,12 +23,14 @@ import resolve from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
import esbuild from 'rollup-plugin-esbuild';
import imageFiles from 'rollup-plugin-image-files';
+import svgr from '@svgr/rollup';
import dts from 'rollup-plugin-dts';
import json from '@rollup/plugin-json';
import { RollupOptions, OutputOptions } from 'rollup';
import { BuildOptions, Output } from './types';
import { paths } from '../paths';
+import { svgrTemplate } from '../svgrTemplate';
export const makeConfigs = async (
options: BuildOptions,
@@ -89,8 +91,12 @@ export const makeConfigs = async (
exclude: ['**/*.stories.*', '**/*.test.*'],
}),
postcss(),
- imageFiles(),
+ imageFiles({ exclude: '**/*.icon.svg' }),
json(),
+ svgr({
+ include: '**/*.icon.svg',
+ template: svgrTemplate,
+ }),
esbuild({
target: 'es2019',
}),
diff --git a/packages/cli/src/lib/svgrTemplate.ts b/packages/cli/src/lib/svgrTemplate.ts
new file mode 100644
index 0000000000..5f7c7f9a4c
--- /dev/null
+++ b/packages/cli/src/lib/svgrTemplate.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This template, together with loaders in the bundler and packages, allows
+ * for SVG to be imported directly as MUI SvgIcon components by suffixing
+ * them with .icon.svg
+ */
+export function svgrTemplate(
+ { template }: any,
+ _opts: any,
+ { imports, interfaces, componentName, jsx }: any,
+) {
+ const iconName = {
+ ...componentName,
+ name: `${componentName.name.replace(/icon$/, '')}Icon`,
+ };
+
+ const defaultExport = {
+ type: 'ExportDefaultDeclaration',
+ declaration: iconName,
+ };
+
+ const typeScriptTemplate = template.smart({ plugins: ['typescript'] });
+ return typeScriptTemplate.ast`
+${imports}
+import SvgIcon from '@material-ui/core/SvgIcon';
+
+${interfaces}
+
+const ${iconName} = props => React.createElement(SvgIcon, props, ${jsx.children});
+
+${defaultExport}`;
+}
diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts
new file mode 100644
index 0000000000..8cb5a1e7df
--- /dev/null
+++ b/packages/cli/src/types.d.ts
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+declare namespace NodeJS {
+ interface ProcessEnv {
+ readonly NODE_ENV: 'development' | 'production' | 'test';
+ }
+}
+
+declare module 'rollup-plugin-image-files' {
+ export default function image(options?: any): any;
+}
+
+declare module '@svgr/rollup' {
+ export default function svgr(options?: any): any;
+}
diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs
index a96061a5bd..c70e00e4d0 100644
--- a/packages/cli/templates/default-app/package.json.hbs
+++ b/packages/cli/templates/default-app/package.json.hbs
@@ -31,6 +31,9 @@
"lerna": "^3.20.2",
"prettier": "^1.19.1"
},
+ "resolutions": {
+ "**/esbuild": "0.5.3"
+ },
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs
index 346e09a76a..31e6baa63e 100644
--- a/packages/cli/templates/default-app/packages/app/package.json.hbs
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -8,20 +8,22 @@
"@material-ui/lab": "4.0.0-alpha.45",
"@backstage/cli": "^{{version}}",
"@backstage/core": "^{{version}}",
+ "@backstage/test-utils": "^{{version}}",
"@backstage/theme": "^{{version}}",
"plugin-welcome": "0.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
+ "react-router": "6.0.0-alpha.5",
"react-router-dom": "6.0.0-alpha.5",
"react-use": "^14.2.0"
},
"devDependencies": {
- "@testing-library/jest-dom": "^5.7.0",
- "@testing-library/react": "^9.3.2",
- "@testing-library/user-event": "^10.2.4",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^10.4.1",
+ "@testing-library/user-event": "^12.0.7",
"@types/jest": "^25.2.2",
"@types/node": "^12.0.0",
- "@types/testing-library__jest-dom": "^5.0.4",
+ "@types/react-dom": "^16.9.8",
"cross-env": "^7.0.0",
"cypress": "^4.2.0",
"eslint-plugin-cypress": "^2.10.3",
diff --git a/packages/cli/templates/default-app/packages/app/public/index.html b/packages/cli/templates/default-app/packages/app/public/index.html
index 3d01107696..ea9208ca57 100644
--- a/packages/cli/templates/default-app/packages/app/public/index.html
+++ b/packages/cli/templates/default-app/packages/app/public/index.html
@@ -8,47 +8,38 @@
name="description"
content="Backstage is an open platform for building developer portals"
/>
-
+
-
-
-
+
+
- Backstage
+ <%= app.title %>
-
+