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 %> - +