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/docs/FAQ.md b/docs/FAQ.md
index 95d705ebe7..85c8f09bef 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -156,7 +156,7 @@ Integrators also configure closed source plugins locally from the monorepo.
We chose GitHub because it is the tool that we are most familiar with, so that
will naturally lead to integrations for GitHub being developed at an early
stage. Hosting this project on GitHub does not exclude integrations with
-alternatives, such as GitLab or Bitbucket. We believe that in time there will be
+alternatives, such as [GitLab](https://github.com/spotify/backstage/issues?q=is%3Aissue+is%3Aopen+GitLab) or Bitbucket. We believe that in time there will be
plugins that will provide functionality for these tools as well. Hopefully,
contributed by the community! Also note, implementations of Backstage can be
hosted wherever you feel suits your needs best.
diff --git a/packages/app/package.json b/packages/app/package.json
index 6aa82a0b36..1c643b437d 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -15,6 +15,7 @@
"@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/test-utils": "^0.1.1-alpha.9",
"@backstage/theme": "^0.1.1-alpha.9",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -22,6 +23,7 @@
"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"
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/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts
index 3d4252b378..bd6454c537 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,9 +56,19 @@
*/
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);
+ }
}
}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 7a0ad95e67..516208bf3b 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -19,6 +19,8 @@
"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",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 0a9d852222..df0d08fa7d 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -27,6 +27,8 @@ import {
getRootLogger,
useHotMemoize,
} from '@backstage/backend-common';
+import { ConfigReader, AppConfig } from '@backstage/config';
+import { loadConfig } from '@backstage/config-loader';
import knex from 'knex';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
@@ -35,20 +37,26 @@ import scaffolder from './plugins/scaffolder';
import sentry from './plugins/sentry';
import { PluginEnvironment } from './types';
-function createEnv(plugin: string): PluginEnvironment {
- const logger = getRootLogger().child({ type: 'plugin', plugin });
- const database = knex({
- client: 'sqlite3',
- connection: ':memory:',
- useNullAsDefault: true,
- });
- database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
- resource.run('PRAGMA foreign_keys = ON', () => {});
- });
- return { logger, database };
+function makeCreateEnv(loadedConfigs: AppConfig[]) {
+ const config = ConfigReader.fromConfigs(loadedConfigs);
+
+ return (plugin: string): PluginEnvironment => {
+ const logger = getRootLogger().child({ type: 'plugin', plugin });
+ const database = knex({
+ client: 'sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ });
+ database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
+ resource.run('PRAGMA foreign_keys = ON', () => {});
+ });
+ return { logger, database, config };
+ };
}
async function main() {
+ const createEnv = makeCreateEnv(await loadConfig());
+
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts
index 7cf9610dc2..a9c687cbc4 100644
--- a/packages/backend/src/plugins/auth.ts
+++ b/packages/backend/src/plugins/auth.ts
@@ -17,6 +17,9 @@
import { createRouter } from '@backstage/plugin-auth-backend';
import { PluginEnvironment } from '../types';
-export default async function createPlugin({ logger }: PluginEnvironment) {
- return await createRouter({ logger });
+export default async function createPlugin({
+ logger,
+ config,
+}: PluginEnvironment) {
+ return await createRouter({ logger, config });
}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index ce681a6bdd..f7df3d05c6 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -16,8 +16,10 @@
import Knex from 'knex';
import { Logger } from 'winston';
+import { Config } from '@backstage/config';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
+ config: Config;
};
diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json
index 60208aeebb..2516263e8a 100644
--- a/packages/catalog-model/package.json
+++ b/packages/catalog-model/package.json
@@ -1,9 +1,7 @@
{
"name": "@backstage/catalog-model",
"version": "0.1.1-alpha.9",
- "main": "dist/index.cjs.js",
- "module": "dist/index.esm.js",
- "main:src": "src/index.ts",
+ "main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
@@ -22,8 +20,10 @@
"clean": "backstage-cli clean"
},
"dependencies": {
+ "@backstage/config": "^0.1.1-alpha.9",
"@types/yup": "^0.28.2",
"lodash": "^4.17.15",
+ "uuid": "^8.0.0",
"yup": "^0.28.5"
},
"devDependencies": {
diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts
index 70d2ba4d2b..4f245da8b7 100644
--- a/packages/catalog-model/src/entity/Entity.ts
+++ b/packages/catalog-model/src/entity/Entity.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { JsonObject } from '@backstage/config';
+
/**
* The format envelope that's common to all versions/kinds of entity.
*
@@ -39,7 +41,7 @@ export type Entity = {
/**
* The specification data describing the entity itself.
*/
- spec?: object;
+ spec?: JsonObject;
};
/**
@@ -48,7 +50,7 @@ export type Entity = {
* @see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
* @see https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/
*/
-export type EntityMeta = {
+export type EntityMeta = JsonObject & {
/**
* A globally unique ID for the entity.
*
@@ -112,3 +114,8 @@ export type EntityMeta = {
*/
annotations?: Record;
};
+
+/**
+ * The keys of EntityMeta that are auto-generated.
+ */
+export const entityMetaGeneratedFields = ['uid', 'etag', 'generation'] as const;
diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts
index 9e96021336..380f5458cc 100644
--- a/packages/catalog-model/src/entity/index.ts
+++ b/packages/catalog-model/src/entity/index.ts
@@ -14,5 +14,12 @@
* limitations under the License.
*/
+export { entityMetaGeneratedFields } from './Entity';
export type { Entity, EntityMeta } from './Entity';
export * from './policies';
+export {
+ entityHasChanges,
+ generateEntityEtag,
+ generateEntityUid,
+ generateUpdatedEntity,
+} from './util';
diff --git a/packages/catalog-model/src/entity/util.test.ts b/packages/catalog-model/src/entity/util.test.ts
new file mode 100644
index 0000000000..e4399bbe05
--- /dev/null
+++ b/packages/catalog-model/src/entity/util.test.ts
@@ -0,0 +1,204 @@
+/*
+ * 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 lodash from 'lodash';
+import {
+ generateEntityEtag,
+ generateEntityUid,
+ entityHasChanges,
+ generateUpdatedEntity,
+} from './util';
+import { Entity } from './Entity';
+
+describe('util', () => {
+ describe('generateEntityUid', () => {
+ it('generates randomness', () => {
+ expect(generateEntityUid()).not.toEqual('');
+ expect(generateEntityUid()).not.toEqual(generateEntityUid());
+ });
+ });
+
+ describe('generateEntityEtag', () => {
+ it('generates randomness', () => {
+ expect(generateEntityEtag()).not.toEqual('');
+ expect(generateEntityEtag()).not.toEqual(generateEntityEtag());
+ });
+ });
+
+ describe('entityHasChanges', () => {
+ let a: Entity;
+ beforeEach(() => {
+ a = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'name',
+ custom: 'custom',
+ labels: {
+ labelKey: 'labelValue',
+ },
+ annotations: {
+ annotationKey: 'annotationValue',
+ },
+ },
+ spec: {
+ a: 'a',
+ },
+ };
+ });
+
+ it('happy path: clone has no changes', () => {
+ const b = lodash.cloneDeep(a);
+ expect(entityHasChanges(a, b)).toBe(false);
+ });
+
+ it('detects root field changes', () => {
+ let b: any = lodash.cloneDeep(a);
+ b.apiVersion += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.apiVersion;
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ b.kind += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.kind;
+ expect(entityHasChanges(a, b)).toBe(true);
+ });
+
+ it('detects metadata changes', () => {
+ let b: any = lodash.cloneDeep(a);
+ b.metadata.name += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.metadata.custom;
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.metadata.custom;
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ b.metadata.labels.n = 'n';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ b.metadata.labels.labelKey += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ });
+
+ it('detects annotation changes, but not removals', () => {
+ let b: any = lodash.cloneDeep(a);
+ b.metadata.annotations.annotationKey += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ b.metadata.annotations.n = 'n';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.metadata.annotations.annotationKey;
+ expect(entityHasChanges(a, b)).toBe(false);
+ });
+
+ it('detects spec changes', () => {
+ let b: any = lodash.cloneDeep(a);
+ b.spec.a += 'a';
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ delete b.spec.a;
+ expect(entityHasChanges(a, b)).toBe(true);
+ b = lodash.cloneDeep(a);
+ b.spec.n = 'n';
+ expect(entityHasChanges(a, b)).toBe(true);
+ });
+ });
+
+ describe('generateUpdatedEntity', () => {
+ let a: Entity;
+ let b: any;
+ beforeEach(() => {
+ a = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ uid: 'da921f56-f655-4e6e-9b8b-bb19a57818d8',
+ etag: 'NzY5NDA5NzQtYmEwNC00MDY0LWFiYmItNTYxYzQxM2JhZDcx',
+ generation: 2,
+ name: 'name',
+ custom: 'custom',
+ labels: {
+ labelKey: 'labelValue',
+ },
+ annotations: {
+ annotationKey: 'annotationValue',
+ },
+ },
+ spec: {
+ a: 'a',
+ },
+ };
+ b = lodash.cloneDeep(a);
+ delete b.metadata.uid;
+ delete b.metadata.etag;
+ delete b.metadata.generation;
+ });
+
+ it('happy path: running on itself leaves it unchanged', () => {
+ const result = generateUpdatedEntity(a, b);
+ expect(result).toEqual(a);
+ });
+
+ it('bumps etag and generation when spec is changed', () => {
+ b.spec.a += 'a';
+ const result = generateUpdatedEntity(a, b);
+ expect(result.metadata.uid).toEqual(a.metadata.uid);
+ expect(result.metadata.etag).not.toEqual(a.metadata.etag);
+ expect(result.metadata.generation).toEqual(a.metadata.generation! + 1);
+ expect(result.spec).toEqual({ a: 'aa' });
+ });
+
+ it('bumps only etag when other things than spec are changed', () => {
+ b.metadata.n = 'n';
+ const result = generateUpdatedEntity(a, b);
+ expect(result.metadata.uid).toEqual(a.metadata.uid);
+ expect(result.metadata.etag).not.toEqual(a.metadata.etag);
+ expect(result.metadata.generation).toEqual(a.metadata.generation);
+ expect(result.metadata.n).toEqual('n');
+ });
+
+ it('retains new annotations', () => {
+ b.metadata.annotations.annotationKey = 'changedValue';
+ b.metadata.annotations.newKey = 'newValue';
+ const result = generateUpdatedEntity(a, b);
+ expect(result.metadata.uid).toEqual(a.metadata.uid);
+ expect(result.metadata.etag).not.toEqual(a.metadata.etag);
+ expect(result.metadata.generation).toEqual(a.metadata.generation);
+ expect(result.metadata.annotations).toEqual({
+ annotationKey: 'changedValue',
+ newKey: 'newValue',
+ });
+ });
+
+ it('retains old annotations', () => {
+ b.metadata.annotations.newKey = 'newValue';
+ const result = generateUpdatedEntity(a, b);
+ expect(result.metadata.uid).toEqual(a.metadata.uid);
+ expect(result.metadata.etag).not.toEqual(a.metadata.etag);
+ expect(result.metadata.generation).toEqual(a.metadata.generation);
+ expect(result.metadata.annotations).toEqual({
+ annotationKey: 'annotationValue',
+ newKey: 'newValue',
+ });
+ });
+ });
+});
diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts
new file mode 100644
index 0000000000..2a196a65d8
--- /dev/null
+++ b/packages/catalog-model/src/entity/util.ts
@@ -0,0 +1,140 @@
+/*
+ * 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 lodash from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
+import { Entity } from './Entity';
+
+/**
+ * Generates a new random UID for an entity.
+ *
+ * @returns A string with enough randomness to uniquely identify an entity
+ */
+export function generateEntityUid(): string {
+ return uuidv4();
+}
+
+/**
+ * Generates a new random Etag for an entity.
+ *
+ * @returns A string with enough randomness to uniquely identify an entity
+ * revision
+ */
+export function generateEntityEtag(): string {
+ return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, '');
+}
+
+/**
+ * Checks whether there are any significant changes going from the previous to
+ * the next version of this entity.
+ *
+ * Significance, in this case, means that we do not compare generated fields
+ * such as uid, etag and generation, and we only check that no new annotations
+ * are added or existing annotations were changed (since they are effectively
+ * merged when doing updates).
+ *
+ * @param previous The old state of the entity
+ * @param next The new state of the entity
+ */
+export function entityHasChanges(previous: Entity, next: Entity): boolean {
+ if (entityHasAnnotationChanges(previous, next)) {
+ return true;
+ }
+
+ const e1 = lodash.cloneDeep(previous);
+ const e2 = lodash.cloneDeep(next);
+
+ if (!e1.metadata.labels) {
+ e1.metadata.labels = {};
+ }
+ if (!e2.metadata.labels) {
+ e2.metadata.labels = {};
+ }
+
+ // Remove generated fields
+ delete e1.metadata.uid;
+ delete e1.metadata.etag;
+ delete e1.metadata.generation;
+ delete e2.metadata.uid;
+ delete e2.metadata.etag;
+ delete e2.metadata.generation;
+
+ // Remove already compared things
+ delete e1.metadata.annotations;
+ delete e2.metadata.annotations;
+
+ return !lodash.isEqual(e1, e2);
+}
+
+/**
+ * Takes an old revision of an entity and a new desired state, and merges
+ * them into a complete new state.
+ *
+ * The previous revision is expected to be a complete model loaded from the
+ * catalog, including the uid, etag and generation fields.
+ *
+ * @param previous The old state of the entity
+ * @param next The new state of the entity
+ * @returns An entity with the merged state of both
+ */
+export function generateUpdatedEntity(previous: Entity, next: Entity): Entity {
+ const { uid, etag, generation } = previous.metadata;
+ if (!uid || !etag || !generation) {
+ throw new Error('Previous entity must have uid, etag and generation');
+ }
+
+ const result = lodash.cloneDeep(next);
+
+ // Annotations are merged, with the new ones taking precedence
+ if (previous.metadata.annotations) {
+ next.metadata.annotations = {
+ ...previous.metadata.annotations,
+ ...next.metadata.annotations,
+ };
+ }
+
+ // Generated fields are copied and updated
+ const bumpEtag = entityHasChanges(previous, result);
+ const bumpGeneration = !lodash.isEqual(previous.spec, result.spec);
+ result.metadata.uid = uid;
+ result.metadata.etag = bumpEtag ? generateEntityEtag() : etag;
+ result.metadata.generation = bumpGeneration ? generation + 1 : generation;
+
+ return result;
+}
+
+function entityHasAnnotationChanges(previous: Entity, next: Entity): boolean {
+ // Since the next annotations get merged into the previous, extract only
+ // the overlapping keys and check if their values match.
+ if (next.metadata.annotations) {
+ if (!previous.metadata.annotations) {
+ return true;
+ }
+ if (
+ !lodash.isEqual(
+ next.metadata.annotations,
+ lodash.pick(
+ previous.metadata.annotations,
+ Object.keys(next.metadata.annotations),
+ ),
+ )
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js
index 4567d8c593..ad24976ad0 100644
--- a/packages/cli/config/jest.js
+++ b/packages/cli/config/jest.js
@@ -25,32 +25,13 @@ async function getConfig() {
return require(path.resolve('jest.config.ts'));
}
- const moduleNameMapper = {
- '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
- };
-
- // Only point to src/ if we're not in CI, there we just build packages first anyway
- if (!process.env.CI) {
- const LernaProject = require('@lerna/project');
- const project = new LernaProject(path.resolve('.'));
- const packages = await project.getPackages();
-
- // To avoid having to build all deps inside the monorepo before running tests,
- // we point directory to src/ where applicable.
- // For example, @backstage/core = /packages/core/src/index.ts is added to moduleNameMapper
- for (const pkg of packages) {
- const mainSrc = pkg.get('main:src');
- if (mainSrc) {
- moduleNameMapper[`^${pkg.name}$`] = path.resolve(pkg.location, mainSrc);
- }
- }
- }
-
const options = {
rootDir: path.resolve('src'),
coverageDirectory: path.resolve('coverage'),
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
- moduleNameMapper,
+ moduleNameMapper: {
+ '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
+ },
// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed
// TODO: jest is working on module support, it's possible that we can remove this in the future
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts
index 50d174368a..aab244f7e9 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/lib/bundler/config.ts
@@ -33,9 +33,6 @@ import { BundlingOptions, BackendBundlingOptions } from './types';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
- if (!baseUrl) {
- throw new Error('app.baseUrl must be set in config');
- }
try {
return new URL(baseUrl, 'http://localhost:3000');
} catch (error) {
@@ -52,9 +49,6 @@ export function createConfig(
const { plugins, loaders } = transforms(options);
const baseUrl = options.config.getString('app.baseUrl');
- if (!baseUrl) {
- throw new Error('app.baseUrl must be set in config');
- }
const validBaseUrl = new URL(baseUrl, 'https://backstage-app.dev');
if (checksEnabled) {
@@ -115,7 +109,7 @@ export function createConfig(
entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
- mainFields: ['main:src', 'browser', 'module', 'main'],
+ mainFields: ['browser', 'module', 'main'],
plugins: [
new ModuleScopePlugin(
[paths.targetSrc, paths.targetDev],
@@ -184,12 +178,11 @@ 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'],
- mainFields: ['main:src', 'browser', 'module', 'main'],
+ mainFields: ['browser', 'module', 'main'],
modules: [paths.targetNodeModules, paths.rootNodeModules],
plugins: [
new ModuleScopePlugin(
diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts
index 481241a869..b6e2bebb41 100644
--- a/packages/cli/src/lib/tasks.ts
+++ b/packages/cli/src/lib/tasks.ts
@@ -171,9 +171,7 @@ export async function installWithLocalDeps(dir: string) {
});
// This takes care of pointing all the installed packages from this repo to
- // dist instead of the local src.
- // For example node_modules/@backstage/core/packages.json is rewritten to point
- // types to dist/index.d.ts and the main:src field is removed.
+ // dist instead of the local src, using the field overrides in publishConfig.
// Without this we get type checking errors in the e2e test
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
Task.section('Patching local dependencies for e2e tests');
@@ -192,7 +190,6 @@ export async function installWithLocalDeps(dir: string) {
const depJson = await fs.readJson(depJsonPath);
// We want dist to be used for e2e tests
- delete depJson['main:src'];
for (const key of Object.keys(depJson.publishConfig)) {
if (key !== 'access') {
depJson[key] = depJson.publishConfig[key];
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..79120e0d7e 100644
--- a/packages/cli/templates/default-app/packages/app/package.json.hbs
+++ b/packages/cli/templates/default-app/packages/app/package.json.hbs
@@ -8,10 +8,12 @@
"@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"
},
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 %>
-
+