) {
- return function withApisWrapper(
- WrappedComponent: React.ComponentType
,
- ) {
- const Hoc = (props: PropsWithChildren>) => {
- const apiHolder = useApiHolder();
-
- const impls = {} as T;
-
- for (const key in apis) {
- if (apis.hasOwnProperty(key)) {
- const ref = apis[key];
-
- const api = apiHolder.get(ref);
- if (!api) {
- throw new Error(`No implementation available for ${ref}`);
- }
- impls[key] = api;
- }
- }
-
- return ;
- };
- const displayName =
- WrappedComponent.displayName || WrappedComponent.name || 'Component';
-
- Hoc.displayName = `withApis(${displayName})`;
-
- return Hoc;
- };
-}
diff --git a/packages/core-api/src/apis/system/ApiRef.test.ts b/packages/core-api/src/apis/system/ApiRef.test.ts
deleted file mode 100644
index 7cd778634c..0000000000
--- a/packages/core-api/src/apis/system/ApiRef.test.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { createApiRef } from './ApiRef';
-
-describe('ApiRef', () => {
- it('should be created', () => {
- const ref = createApiRef({ id: 'abc', description: '123' });
- expect(ref.id).toBe('abc');
- expect(ref.description).toBe('123');
- expect(String(ref)).toBe('apiRef{abc}');
- expect(() => ref.T).toThrow('tried to read ApiRef.T of apiRef{abc}');
- });
-
- it('should reject invalid ids', () => {
- for (const id of ['a', 'abc', 'ab-c', 'a.b.c', 'a-b.c', 'abc.a-b-c.abc3']) {
- expect(createApiRef({ id, description: '123' }).id).toBe(id);
- }
-
- for (const id of [
- '123',
- 'ab-3',
- 'ab_c',
- '.',
- '2ac',
- 'ab.3a',
- '.abc',
- 'abc.',
- 'ab..s',
- '',
- '_',
- ]) {
- expect(() => createApiRef({ id, description: '123' }).id).toThrow(
- `API id must only contain period separated lowercase alphanum tokens with dashes, got '${id}'`,
- );
- }
- });
-});
diff --git a/packages/core-api/src/apis/system/ApiRef.ts b/packages/core-api/src/apis/system/ApiRef.ts
deleted file mode 100644
index 5a774474d8..0000000000
--- a/packages/core-api/src/apis/system/ApiRef.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 type { ApiRef } from './types';
-
-export type ApiRefConfig = {
- id: string;
- description?: string;
-};
-
-class ApiRefImpl implements ApiRef {
- constructor(private readonly config: ApiRefConfig) {
- const valid = config.id
- .split('.')
- .flatMap(part => part.split('-'))
- .every(part => part.match(/^[a-z][a-z0-9]*$/));
- if (!valid) {
- throw new Error(
- `API id must only contain period separated lowercase alphanum tokens with dashes, got '${config.id}'`,
- );
- }
- }
-
- get id(): string {
- return this.config.id;
- }
-
- get description() {
- return this.config.description;
- }
-
- // Utility for getting type of an api, using `typeof apiRef.T`
- get T(): T {
- throw new Error(`tried to read ApiRef.T of ${this}`);
- }
-
- toString() {
- return `apiRef{${this.config.id}}`;
- }
-}
-
-export function createApiRef(config: ApiRefConfig): ApiRef {
- return new ApiRefImpl(config);
-}
diff --git a/packages/core-api/src/apis/system/ApiRegistry.test.ts b/packages/core-api/src/apis/system/ApiRegistry.test.ts
deleted file mode 100644
index 66fcb70262..0000000000
--- a/packages/core-api/src/apis/system/ApiRegistry.test.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { ApiRegistry } from './ApiRegistry';
-import { createApiRef } from './ApiRef';
-
-describe('ApiRegistry', () => {
- const x1Ref = createApiRef({ id: 'x1', description: '' });
- const x1DuplicateRef = createApiRef({ id: 'x1', description: '' });
- const x2Ref = createApiRef({ id: 'x2', description: '' });
-
- it('should be created', () => {
- const registry = ApiRegistry.from([]);
- expect(registry.get(x1Ref)).toBe(undefined);
- });
-
- it('should be created with APIs', () => {
- const registry = ApiRegistry.from([
- [x1Ref, 3],
- [x2Ref, 'y'],
- ]);
- expect(registry.get(x1Ref)).toBe(3);
- expect(registry.get(x1DuplicateRef)).toBe(3);
- expect(registry.get(x2Ref)).toBe('y');
- });
-
- it('should be built', () => {
- const registry = ApiRegistry.builder().build();
- expect(registry.get(x1Ref)).toBe(undefined);
- expect(registry.get(x1DuplicateRef)).toBe(undefined);
- });
-
- it('should be built with APIs', () => {
- const builder = ApiRegistry.builder();
- builder.add(x1Ref, 3);
- builder.add(x2Ref, 'y');
-
- const registry = builder.build();
- expect(registry.get(x1Ref)).toBe(3);
- expect(registry.get(x1DuplicateRef)).toBe(3);
- expect(registry.get(x2Ref)).toBe('y');
- });
-
- it('should be created with API', () => {
- const reg1 = ApiRegistry.with(x1Ref, 3);
- const reg2 = reg1.with(x2Ref, 'y');
- const reg3 = reg2.with(x2Ref, 'z');
- const reg4 = reg3.with(x1Ref, 2);
- const reg5 = reg3.with(x1DuplicateRef, 4);
-
- expect(reg1.get(x1Ref)).toBe(3);
- expect(reg1.get(x2Ref)).toBe(undefined);
- expect(reg2.get(x1Ref)).toBe(3);
- expect(reg2.get(x2Ref)).toBe('y');
- expect(reg3.get(x1Ref)).toBe(3);
- expect(reg3.get(x2Ref)).toBe('z');
- expect(reg4.get(x1Ref)).toBe(2);
- expect(reg4.get(x2Ref)).toBe('z');
- expect(reg5.get(x1Ref)).toBe(4);
- expect(reg5.get(x2Ref)).toBe('z');
- });
-});
diff --git a/packages/core-api/src/apis/system/ApiRegistry.ts b/packages/core-api/src/apis/system/ApiRegistry.ts
deleted file mode 100644
index 6fcfe03f62..0000000000
--- a/packages/core-api/src/apis/system/ApiRegistry.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { ApiRef, ApiHolder } from './types';
-
-type ApiImpl = readonly [ApiRef, T];
-
-class ApiRegistryBuilder {
- private apis: [string, unknown][] = [];
-
- add(api: ApiRef, impl: I): I {
- this.apis.push([api.id, impl]);
- return impl;
- }
-
- build(): ApiRegistry {
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- return new ApiRegistry(new Map(this.apis));
- }
-}
-
-export class ApiRegistry implements ApiHolder {
- static builder() {
- return new ApiRegistryBuilder();
- }
-
- static from(apis: ApiImpl[]) {
- return new ApiRegistry(new Map(apis.map(([api, impl]) => [api.id, impl])));
- }
-
- /**
- * Creates a new ApiRegistry with a single API implementation.
- *
- * @param api ApiRef for the API to add
- * @param impl Implementation of the API to add
- */
- static with(api: ApiRef, impl: T): ApiRegistry {
- return new ApiRegistry(new Map([[api.id, impl]]));
- }
-
- constructor(private readonly apis: Map) {}
-
- /**
- * Returns a new ApiRegistry with the provided API added to the existing ones.
- *
- * @param api ApiRef for the API to add
- * @param impl Implementation of the API to add
- */
- with(api: ApiRef, impl: T): ApiRegistry {
- return new ApiRegistry(new Map([...this.apis, [api.id, impl]]));
- }
-
- get(api: ApiRef): T | undefined {
- return this.apis.get(api.id) as T | undefined;
- }
-}
diff --git a/packages/core-api/src/apis/system/ApiResolver.test.ts b/packages/core-api/src/apis/system/ApiResolver.test.ts
deleted file mode 100644
index 064a4f8d77..0000000000
--- a/packages/core-api/src/apis/system/ApiResolver.test.ts
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { ApiResolver } from './ApiResolver';
-import { createApiRef } from './ApiRef';
-import { ApiFactoryRegistry } from './ApiFactoryRegistry';
-
-const aRef = createApiRef({ id: 'a', description: '' });
-const otherARef = createApiRef({ id: 'a', description: 'other' });
-const bRef = createApiRef({ id: 'b', description: '' });
-const otherBRef = createApiRef({ id: 'b', description: 'other' });
-const cRef = createApiRef<{ x: string }>({ id: 'c', description: '' });
-const otherCRef = createApiRef<{ x: string }>({
- id: 'c',
- description: 'other',
-});
-
-function createRegistry() {
- const registry = new ApiFactoryRegistry();
- registry.register('default', {
- api: aRef,
- deps: {},
- factory: () => 1,
- });
- registry.register('default', {
- api: bRef,
- deps: {},
- factory: () => 'b',
- });
- registry.register('default', {
- api: cRef,
- deps: { b: otherBRef },
- factory: ({ b }) => ({ x: 'x', b }),
- });
- return registry;
-}
-
-function createSelfCyclicRegistry() {
- const registry = new ApiFactoryRegistry();
- registry.register('default', {
- api: aRef,
- deps: { a: aRef },
- factory: () => 1,
- });
- return registry;
-}
-
-function createShortCyclicRegistry() {
- const registry = new ApiFactoryRegistry();
- registry.register('default', {
- api: aRef,
- deps: { b: bRef },
- factory: () => 1,
- });
- registry.register('default', {
- api: bRef,
- deps: { a: aRef },
- factory: () => 'x',
- });
- return registry;
-}
-
-function createShortCyclicRegistryWithOther() {
- const registry = new ApiFactoryRegistry();
- registry.register('default', {
- api: aRef,
- deps: { b: bRef },
- factory: () => 1,
- });
- registry.register('default', {
- api: otherBRef,
- deps: { a: otherARef },
- factory: () => 'x',
- });
- return registry;
-}
-
-function createLongCyclicRegistry() {
- const registry = new ApiFactoryRegistry();
- registry.register('default', {
- api: aRef,
- deps: { b: otherBRef },
- factory: () => 1,
- });
- registry.register('default', {
- api: bRef,
- deps: { c: cRef },
- factory: () => 'b',
- });
- registry.register('default', {
- api: cRef,
- deps: { a: aRef },
- factory: () => ({ x: 'x' }),
- });
- return registry;
-}
-
-describe('ApiResolver', () => {
- it('should be created empty', () => {
- const resolver = new ApiResolver(new ApiFactoryRegistry());
- expect(resolver.get(aRef)).toBe(undefined);
- expect(resolver.get(bRef)).toBe(undefined);
- expect(resolver.get(otherBRef)).toBe(undefined);
- expect(resolver.get(cRef)).toBe(undefined);
- });
-
- it('should instantiate APIs', () => {
- const resolver = new ApiResolver(createRegistry());
- expect(resolver.get(aRef)).toBe(1);
- expect(resolver.get(otherARef)).toBe(1);
- expect(resolver.get(bRef)).toBe('b');
- expect(resolver.get(otherBRef)).toBe('b');
- expect(resolver.get(cRef)).toEqual({ x: 'x', b: 'b' });
- expect(resolver.get(cRef)).toBe(resolver.get(otherCRef));
- });
-
- it('should detect self dependency cycles', () => {
- const resolver = new ApiResolver(createSelfCyclicRegistry());
- expect(() => resolver.get(aRef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- });
-
- it('should detect short dependency cycles', () => {
- const resolver = new ApiResolver(createShortCyclicRegistry());
- expect(() => resolver.get(aRef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => resolver.get(bRef)).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- });
-
- it('should detect short dependency cycles with other refs', () => {
- const resolver = new ApiResolver(createShortCyclicRegistryWithOther());
- expect(() => resolver.get(aRef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => resolver.get(bRef)).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => resolver.get(otherARef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => resolver.get(otherBRef)).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- });
-
- it('should detect long dependency cycles', () => {
- const resolver = new ApiResolver(createLongCyclicRegistry());
- expect(() => resolver.get(aRef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- // Second call for same ref should still throw
- expect(() => resolver.get(aRef)).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => resolver.get(bRef)).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => resolver.get(otherBRef)).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => resolver.get(cRef)).toThrow(
- 'Circular dependency of api factory for apiRef{c}',
- );
- });
-
- it('should validate a factory holder', () => {
- expect(() => {
- ApiResolver.validateFactories(createRegistry(), [
- aRef,
- bRef,
- otherBRef,
- cRef,
- ]);
- }).not.toThrow();
- });
-
- it('should find self cycles with validation', () => {
- const self = createSelfCyclicRegistry();
- expect(() => ApiResolver.validateFactories(self, [aRef])).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => ApiResolver.validateFactories(self, [otherARef])).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- });
-
- it('should find dependency cycles with validation', () => {
- const short = createShortCyclicRegistry();
- expect(() => ApiResolver.validateFactories(short, [aRef])).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => ApiResolver.validateFactories(short, [otherARef])).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() => ApiResolver.validateFactories(short, [bRef])).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => ApiResolver.validateFactories(short, [otherBRef])).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
-
- const shortOther = createShortCyclicRegistryWithOther();
- expect(() => ApiResolver.validateFactories(shortOther, [aRef])).toThrow(
- 'Circular dependency of api factory for apiRef{a}',
- );
- expect(() =>
- ApiResolver.validateFactories(shortOther, [otherARef]),
- ).toThrow('Circular dependency of api factory for apiRef{a}');
- expect(() => ApiResolver.validateFactories(shortOther, [bRef])).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() =>
- ApiResolver.validateFactories(shortOther, [otherBRef]),
- ).toThrow('Circular dependency of api factory for apiRef{b}');
-
- const long = createLongCyclicRegistry();
- expect(() =>
- ApiResolver.validateFactories(long, long.getAllApis()),
- ).toThrow('Circular dependency of api factory for apiRef{a}');
- expect(() => ApiResolver.validateFactories(long, [bRef])).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => ApiResolver.validateFactories(long, [otherBRef])).toThrow(
- 'Circular dependency of api factory for apiRef{b}',
- );
- expect(() => ApiResolver.validateFactories(long, [cRef])).toThrow(
- 'Circular dependency of api factory for apiRef{c}',
- );
- });
-
- it('should only call factory func once', () => {
- const registry = new ApiFactoryRegistry();
- const factory = jest.fn().mockReturnValue(2);
- registry.register('default', {
- api: aRef,
- deps: {},
- factory,
- });
-
- const resolver = new ApiResolver(registry);
- expect(factory).toHaveBeenCalledTimes(0);
- expect(resolver.get(aRef)).toBe(2);
- expect(factory).toHaveBeenCalledTimes(1);
- expect(resolver.get(aRef)).toBe(2);
- expect(factory).toHaveBeenCalledTimes(1);
- expect(resolver.get(otherARef)).toBe(2);
- expect(factory).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/packages/core-api/src/apis/system/ApiResolver.ts b/packages/core-api/src/apis/system/ApiResolver.ts
deleted file mode 100644
index 4d69067b43..0000000000
--- a/packages/core-api/src/apis/system/ApiResolver.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 {
- ApiRef,
- ApiHolder,
- ApiFactoryHolder,
- AnyApiRef,
- TypesToApiRefs,
-} from './types';
-
-export class ApiResolver implements ApiHolder {
- /**
- * Validate factories by making sure that each of the apis can be created
- * without hitting any circular dependencies.
- */
- static validateFactories(
- factories: ApiFactoryHolder,
- apis: Iterable,
- ) {
- for (const api of apis) {
- const heap = [api];
- const allDeps = new Set();
-
- while (heap.length) {
- const apiRef = heap.shift()!;
- const factory = factories.get(apiRef);
- if (!factory) {
- continue;
- }
-
- for (const dep of Object.values(factory.deps)) {
- if (dep.id === api.id) {
- throw new Error(`Circular dependency of api factory for ${api}`);
- }
- if (!allDeps.has(dep)) {
- allDeps.add(dep);
- heap.push(dep);
- }
- }
- }
- }
- }
-
- private readonly apis = new Map();
-
- constructor(private readonly factories: ApiFactoryHolder) {}
-
- get(ref: ApiRef): T | undefined {
- return this.load(ref);
- }
-
- private load(ref: ApiRef, loading: AnyApiRef[] = []): T | undefined {
- const impl = this.apis.get(ref.id);
- if (impl) {
- return impl as T;
- }
-
- const factory = this.factories.get(ref);
- if (!factory) {
- return undefined;
- }
-
- if (loading.includes(factory.api)) {
- throw new Error(`Circular dependency of api factory for ${factory.api}`);
- }
-
- const deps = this.loadDeps(ref, factory.deps, [...loading, factory.api]);
- const api = factory.factory(deps);
- this.apis.set(ref.id, api);
- return api as T;
- }
-
- private loadDeps(
- dependent: ApiRef,
- apis: TypesToApiRefs,
- loading: AnyApiRef[],
- ): T {
- const impls = {} as T;
-
- for (const key in apis) {
- if (apis.hasOwnProperty(key)) {
- const ref = apis[key];
-
- const api = this.load(ref, loading);
- if (!api) {
- throw new Error(
- `No API factory available for dependency ${ref} of dependent ${dependent}`,
- );
- }
- impls[key] = api;
- }
- }
-
- return impls;
- }
-}
diff --git a/packages/core-api/src/apis/system/helpers.ts b/packages/core-api/src/apis/system/helpers.ts
deleted file mode 100644
index 8e84dd6c09..0000000000
--- a/packages/core-api/src/apis/system/helpers.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { ApiRef, ApiFactory, TypesToApiRefs } from './types';
-
-/**
- * Used to infer types for a standalone ApiFactory that isn't immediately passed
- * to another function.
- * This function doesn't actually do anything, it's only used to infer types.
- */
-export function createApiFactory<
- Api,
- Impl extends Api,
- Deps extends { [name in string]: unknown }
->(factory: ApiFactory): ApiFactory;
-export function createApiFactory(
- api: ApiRef,
- instance: Impl,
-): ApiFactory;
-export function createApiFactory<
- Api,
- Impl extends Api,
- Deps extends { [name in string]: unknown }
->(
- factory: ApiFactory | ApiRef,
- instance?: Impl,
-): ApiFactory {
- if ('id' in factory) {
- return {
- api: factory,
- deps: {} as TypesToApiRefs,
- factory: () => instance!,
- };
- }
- return factory;
-}
diff --git a/packages/core-api/src/apis/system/index.ts b/packages/core-api/src/apis/system/index.ts
deleted file mode 100644
index c9b9fac936..0000000000
--- a/packages/core-api/src/apis/system/index.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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.
- */
-
-export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
-export { ApiRegistry } from './ApiRegistry';
-export { ApiResolver } from './ApiResolver';
-export { ApiFactoryRegistry } from './ApiFactoryRegistry';
-export { createApiRef } from './ApiRef';
-export * from './types';
-export * from './helpers';
diff --git a/packages/core-api/src/apis/system/types.ts b/packages/core-api/src/apis/system/types.ts
deleted file mode 100644
index 17ceff4649..0000000000
--- a/packages/core-api/src/apis/system/types.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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.
- */
-
-export type ApiRef = {
- id: string;
- description?: string;
- T: T;
-};
-
-export type AnyApiRef = ApiRef;
-
-export type ApiRefType = T extends ApiRef ? U : never;
-
-export type TypesToApiRefs = { [key in keyof T]: ApiRef };
-
-export type ApiRefsToTypes }> = {
- [key in keyof T]: ApiRefType;
-};
-
-export type ApiHolder = {
- get(api: ApiRef): T | undefined;
-};
-
-export type ApiFactory<
- Api,
- Impl extends Api,
- Deps extends { [name in string]: unknown }
-> = {
- api: ApiRef;
- deps: TypesToApiRefs;
- factory(deps: Deps): Impl;
-};
-
-export type AnyApiFactory = ApiFactory<
- unknown,
- unknown,
- { [key in string]: unknown }
->;
-
-export type ApiFactoryHolder = {
- get(
- api: ApiRef,
- ): ApiFactory | undefined;
-};
diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx
deleted file mode 100644
index 5404c111e4..0000000000
--- a/packages/core-api/src/app/App.test.tsx
+++ /dev/null
@@ -1,375 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 {
- configApiRef,
- createApiFactory,
- featureFlagsApiRef,
- LocalStorageFeatureFlags,
-} from '../apis';
-import { renderWithEffects, withLogCollector } from '@backstage/test-utils';
-import { lightTheme } from '@backstage/theme';
-import { render, screen } from '@testing-library/react';
-import React, { PropsWithChildren } from 'react';
-import { BrowserRouter, Routes } from 'react-router-dom';
-import { createRoutableExtension } from '../extensions';
-import { defaultSystemIcons } from '../icons';
-import { createPlugin } from '../plugin';
-import { useRouteRef } from '../routing/hooks';
-import {
- createExternalRouteRef,
- createRouteRef,
- createSubRouteRef,
-} from '../routing';
-import { generateBoundRoutes, PrivateAppImpl } from './App';
-
-describe('generateBoundRoutes', () => {
- it('runs happy path', () => {
- const external = { myRoute: createExternalRouteRef({ id: '1' }) };
- const ref = createRouteRef({ id: 'ref-1' });
- const result = generateBoundRoutes(({ bind }) => {
- bind(external, { myRoute: ref });
- });
-
- expect(result.get(external.myRoute)).toBe(ref);
- });
-
- it('throws on unknown keys', () => {
- const external = { myRoute: createExternalRouteRef({ id: '2' }) };
- const ref = createRouteRef({ id: 'ref-2' });
- expect(() =>
- generateBoundRoutes(({ bind }) => {
- bind(external, { someOtherRoute: ref } as any);
- }),
- ).toThrow('Key someOtherRoute is not an existing external route');
- });
-});
-
-describe('Integration Test', () => {
- const plugin1RouteRef = createRouteRef({ id: 'ref-1' });
- const plugin2RouteRef = createRouteRef({ id: 'ref-2', params: ['x'] });
- const subRouteRef1 = createSubRouteRef({
- id: 'sub1',
- path: '/sub1',
- parent: plugin1RouteRef,
- });
- const subRouteRef2 = createSubRouteRef({
- id: 'sub2',
- path: '/sub2/:x',
- parent: plugin1RouteRef,
- });
- const subRouteRef3 = createSubRouteRef({
- id: 'sub3',
- path: '/sub3',
- parent: plugin2RouteRef,
- });
- const subRouteRef4 = createSubRouteRef({
- id: 'sub4',
- path: '/sub4/:y',
- parent: plugin2RouteRef,
- });
- const extRouteRef1 = createExternalRouteRef({ id: 'extRouteRef1' });
- const extRouteRef2 = createExternalRouteRef({
- id: 'extRouteRef2',
- params: ['x'],
- });
- const extRouteRef3 = createExternalRouteRef({
- id: 'extRouteRef3',
- optional: true,
- });
- const extRouteRef4 = createExternalRouteRef({
- id: 'extRouteRef4',
- optional: true,
- params: ['x'],
- });
-
- const plugin1 = createPlugin({
- id: 'blob',
- // Both absolute and sub route refs should be assignable to the plugin routes
- routes: {
- ref1: plugin1RouteRef,
- ref2: plugin2RouteRef,
- ref3: subRouteRef1,
- },
- externalRoutes: {
- extRouteRef1,
- extRouteRef2,
- extRouteRef3,
- extRouteRef4,
- },
- });
-
- const plugin2 = createPlugin({
- id: 'plugin2',
- });
-
- const HiddenComponent = plugin2.provide(
- createRoutableExtension({
- component: () => Promise.resolve((_: { path?: string }) => ),
- mountPoint: plugin2RouteRef,
- }),
- );
-
- const ExposedComponent = plugin1.provide(
- createRoutableExtension({
- component: () =>
- Promise.resolve((_: PropsWithChildren<{ path?: string }>) => {
- const link1 = useRouteRef(plugin1RouteRef);
- const link2 = useRouteRef(plugin2RouteRef);
- const subLink1 = useRouteRef(subRouteRef1);
- const subLink2 = useRouteRef(subRouteRef2);
- const subLink3 = useRouteRef(subRouteRef3);
- const subLink4 = useRouteRef(subRouteRef4);
- const extLink1 = useRouteRef(extRouteRef1);
- const extLink2 = useRouteRef(extRouteRef2);
- const extLink3 = useRouteRef(extRouteRef3);
- const extLink4 = useRouteRef(extRouteRef4);
- return (
-
- link1: {link1()}
- link2: {link2({ x: 'a' })}
- subLink1: {subLink1()}
- subLink2: {subLink2({ x: 'a' })}
- subLink3: {subLink3({ x: 'b' })}
- subLink4: {subLink4({ x: 'c', y: 'd' })}
- extLink1: {extLink1()}
- extLink2: {extLink2({ x: 'a' })}
- extLink3: {extLink3?.() ?? ''}
- extLink4: {extLink4?.({ x: 'b' }) ?? ''}
-
- );
- }),
- mountPoint: plugin1RouteRef,
- }),
- );
-
- const components = {
- NotFoundErrorPage: () => null,
- BootErrorPage: () => null,
- Progress: () => null,
- Router: BrowserRouter,
- };
-
- it('runs happy paths', async () => {
- const app = new PrivateAppImpl({
- apis: [],
- defaultApis: [],
- themes: [
- {
- id: 'light',
- title: 'Light Theme',
- variant: 'light',
- theme: lightTheme,
- },
- ],
- icons: defaultSystemIcons,
- plugins: [],
- components,
- bindRoutes: ({ bind }) => {
- bind(plugin1.externalRoutes, {
- extRouteRef1: plugin1RouteRef,
- extRouteRef2: plugin2RouteRef,
- extRouteRef3: subRouteRef1,
- extRouteRef4: plugin2RouteRef,
- });
- },
- });
-
- const Provider = app.getProvider();
- const Router = app.getRouter();
-
- await renderWithEffects(
-
-
-
-
-
-
-
- ,
- );
-
- expect(screen.getByText('link1: /')).toBeInTheDocument();
- expect(screen.getByText('link2: /foo/a')).toBeInTheDocument();
- expect(screen.getByText('subLink1: /sub1')).toBeInTheDocument();
- expect(screen.getByText('subLink2: /sub2/a')).toBeInTheDocument();
- expect(screen.getByText('subLink3: /foo/b/sub3')).toBeInTheDocument();
- expect(screen.getByText('subLink4: /foo/c/sub4/d')).toBeInTheDocument();
- expect(screen.getByText('extLink1: /')).toBeInTheDocument();
- expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument();
- expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument();
- expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument();
-
- // Plugins should be discovered through element tree
- expect(app.getPlugins()).toEqual([plugin1, plugin2]);
- });
-
- it('runs happy paths without optional routes', async () => {
- const app = new PrivateAppImpl({
- apis: [],
- defaultApis: [],
- themes: [
- {
- id: 'light',
- title: 'Light Theme',
- variant: 'light',
- theme: lightTheme,
- },
- ],
- icons: defaultSystemIcons,
- plugins: [],
- components,
- bindRoutes: ({ bind }) => {
- bind(plugin1.externalRoutes, {
- extRouteRef1: plugin1RouteRef,
- extRouteRef2: plugin2RouteRef,
- });
- },
- });
-
- const Provider = app.getProvider();
- const Router = app.getRouter();
-
- await renderWithEffects(
-
-
-
-
-
-
-
- ,
- );
-
- expect(screen.getByText('extLink1: /')).toBeInTheDocument();
- expect(screen.getByText('extLink2: /foo')).toBeInTheDocument();
- expect(screen.getByText('extLink3: ')).toBeInTheDocument();
- expect(screen.getByText('extLink4: ')).toBeInTheDocument();
- });
-
- it('should wait for the config to load before calling feature flags', async () => {
- const storageFlags = new LocalStorageFeatureFlags();
- jest.spyOn(storageFlags, 'registerFlag');
-
- const apis = [
- createApiFactory({
- api: featureFlagsApiRef,
- deps: { configApi: configApiRef },
- factory() {
- return storageFlags;
- },
- }),
- ];
-
- const app = new PrivateAppImpl({
- apis,
- defaultApis: [],
- themes: [
- {
- id: 'light',
- title: 'Light Theme',
- variant: 'light',
- theme: lightTheme,
- },
- ],
- icons: defaultSystemIcons,
- plugins: [
- createPlugin({
- id: 'test',
- register: p => p.featureFlags.register('name'),
- }),
- ],
- components,
- bindRoutes: ({ bind }) => {
- bind(plugin1.externalRoutes, {
- extRouteRef1: plugin1RouteRef,
- extRouteRef2: plugin2RouteRef,
- });
- },
- });
-
- const Provider = app.getProvider();
- const Router = app.getRouter();
-
- await renderWithEffects(
-
-
-
-
-
-
-
- ,
- );
-
- expect(storageFlags.registerFlag).toHaveBeenCalledWith({
- name: 'name',
- pluginId: 'test',
- });
- });
-
- it('should throw some error when the route has duplicate params', () => {
- const app = new PrivateAppImpl({
- apis: [],
- defaultApis: [],
- themes: [
- {
- id: 'light',
- title: 'Light Theme',
- variant: 'light',
- theme: lightTheme,
- },
- ],
- icons: defaultSystemIcons,
- plugins: [],
- components,
- bindRoutes: ({ bind }) => {
- bind(plugin1.externalRoutes, {
- extRouteRef1: plugin1RouteRef,
- extRouteRef2: plugin2RouteRef,
- });
- },
- });
-
- const Provider = app.getProvider();
- const Router = app.getRouter();
- const { error: errorLogs } = withLogCollector(() => {
- expect(() =>
- render(
-
-
-
-
-
-
-
-
- ,
- ),
- ).toThrow(
- 'Parameter :thing is duplicated in path /test/:thing/some/:thing',
- );
- });
- expect(errorLogs).toEqual([
- expect.stringContaining(
- 'Parameter :thing is duplicated in path /test/:thing/some/:thing',
- ),
- expect.stringContaining(
- 'The above error occurred in the component',
- ),
- ]);
- });
-});
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
deleted file mode 100644
index 3a5e151466..0000000000
--- a/packages/core-api/src/app/App.tsx
+++ /dev/null
@@ -1,525 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { Config } from '@backstage/config';
-import React, {
- ComponentType,
- PropsWithChildren,
- ReactElement,
- useEffect,
- useMemo,
- useState,
-} from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
-import { useAsync } from 'react-use';
-import {
- AnyApiFactory,
- ApiHolder,
- ApiProvider,
- ApiRegistry,
- AppTheme,
- appThemeApiRef,
- AppThemeSelector,
- configApiRef,
- ConfigReader,
- LocalStorageFeatureFlags,
- useApi,
-} from '../apis';
-import {
- AppThemeApi,
- ConfigApi,
- featureFlagsApiRef,
- identityApiRef,
-} from '../apis/definitions';
-import { ApiFactoryRegistry, ApiResolver } from '../apis/system';
-import {
- childDiscoverer,
- routeElementDiscoverer,
- traverseElementTree,
-} from '../extensions/traversal';
-import { IconComponent, IconComponentMap, IconKey } from '../icons';
-import { BackstagePlugin } from '../plugin';
-import { pluginCollector } from '../plugin/collectors';
-import { AnyRoutes } from '../plugin/types';
-import { RouteRef, ExternalRouteRef, SubRouteRef } from '../routing';
-import {
- routeObjectCollector,
- routeParentCollector,
- routePathCollector,
-} from '../routing/collectors';
-import { RoutingProvider } from '../routing/hooks';
-import { validateRoutes } from '../routing/validation';
-import { AppContextProvider } from './AppContext';
-import { AppIdentity } from './AppIdentity';
-import { AppThemeProvider } from './AppThemeProvider';
-import {
- AppComponents,
- AppConfigLoader,
- AppContext,
- AppOptions,
- AppRouteBinder,
- BackstageApp,
- SignInPageProps,
- SignInResult,
-} from './types';
-
-export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
- const result = new Map();
-
- if (bindRoutes) {
- const bind: AppRouteBinder = (externalRoutes, targetRoutes: AnyRoutes) => {
- for (const [key, value] of Object.entries(targetRoutes)) {
- const externalRoute = externalRoutes[key];
- if (!externalRoute) {
- throw new Error(`Key ${key} is not an existing external route`);
- }
- if (!value && !externalRoute.optional) {
- throw new Error(
- `External route ${key} is required but was undefined`,
- );
- }
- if (value) {
- result.set(externalRoute, value);
- }
- }
- };
- bindRoutes({ bind });
- }
-
- return result;
-}
-
-type FullAppOptions = {
- apis: Iterable;
- icons: IconComponentMap;
- plugins: BackstagePlugin[];
- components: AppComponents;
- themes: AppTheme[];
- configLoader?: AppConfigLoader;
- defaultApis: Iterable;
- bindRoutes?: AppOptions['bindRoutes'];
-};
-
-function useConfigLoader(
- configLoader: AppConfigLoader | undefined,
- components: AppComponents,
- appThemeApi: AppThemeApi,
-): { api: ConfigApi } | { node: JSX.Element } {
- // Keeping this synchronous when a config loader isn't set simplifies tests a lot
- const hasConfig = Boolean(configLoader);
- const config = useAsync(configLoader || (() => Promise.resolve([])));
-
- let noConfigNode = undefined;
-
- if (hasConfig && config.loading) {
- const { Progress } = components;
- noConfigNode = ;
- } else if (config.error) {
- const { BootErrorPage } = components;
- noConfigNode = ;
- }
-
- // Before the config is loaded we can't use a router, so exit early
- if (noConfigNode) {
- return {
- node: (
-
- {noConfigNode}
-
- ),
- };
- }
-
- const configReader = ConfigReader.fromConfigs(config.value ?? []);
-
- return { api: configReader };
-}
-
-class AppContextImpl implements AppContext {
- constructor(private readonly app: PrivateAppImpl) {}
-
- getPlugins(): BackstagePlugin[] {
- // eslint-disable-next-line no-console
- console.warn('appContext.getPlugins() is deprecated and will be removed');
- return this.app.getPlugins();
- }
-
- getSystemIcon(key: IconKey): IconComponent | undefined {
- return this.app.getSystemIcon(key);
- }
-
- getComponents(): AppComponents {
- return this.app.getComponents();
- }
-
- getProvider(): React.ComponentType<{}> {
- // eslint-disable-next-line no-console
- console.warn('appContext.getProvider() is deprecated and will be removed');
- return this.app.getProvider();
- }
-
- getRouter(): React.ComponentType<{}> {
- // eslint-disable-next-line no-console
- console.warn('appContext.getRouter() is deprecated and will be removed');
- return this.app.getRouter();
- }
-
- getRoutes(): JSX.Element[] {
- // eslint-disable-next-line no-console
- console.warn('appContext.getRoutes() is deprecated and will be removed');
- return this.app.getRoutes();
- }
-}
-
-export class PrivateAppImpl implements BackstageApp {
- private apiHolder?: ApiHolder;
- private configApi?: ConfigApi;
-
- private readonly apis: Iterable;
- private readonly icons: IconComponentMap;
- private readonly plugins: Set>;
- private readonly components: AppComponents;
- private readonly themes: AppTheme[];
- private readonly configLoader?: AppConfigLoader;
- private readonly defaultApis: Iterable;
- private readonly bindRoutes: AppOptions['bindRoutes'];
-
- private readonly identityApi = new AppIdentity();
-
- constructor(options: FullAppOptions) {
- this.apis = options.apis;
- this.icons = options.icons;
- this.plugins = new Set(options.plugins);
- this.components = options.components;
- this.themes = options.themes;
- this.configLoader = options.configLoader;
- this.defaultApis = options.defaultApis;
- this.bindRoutes = options.bindRoutes;
- }
-
- getPlugins(): BackstagePlugin[] {
- return Array.from(this.plugins);
- }
-
- getSystemIcon(key: IconKey): IconComponent | undefined {
- return this.icons[key];
- }
-
- getComponents(): AppComponents {
- return this.components;
- }
-
- getRoutes(): JSX.Element[] {
- const routes = new Array();
-
- const { NotFoundErrorPage } = this.components;
-
- for (const plugin of this.plugins.values()) {
- for (const output of plugin.output()) {
- switch (output.type) {
- case 'legacy-route': {
- const { path, component: Component } = output;
- routes.push(
- } />,
- );
- break;
- }
- case 'route': {
- const { target, component: Component } = output;
- routes.push(
- }
- />,
- );
- break;
- }
- case 'legacy-redirect-route': {
- const { path, target } = output;
- routes.push();
- break;
- }
- case 'redirect-route': {
- const { from, to } = output;
- routes.push();
- break;
- }
- default:
- break;
- }
- }
- }
-
- routes.push(
- }
- />,
- );
-
- return routes;
- }
-
- getProvider(): ComponentType<{}> {
- const appContext = new AppContextImpl(this);
-
- const Provider = ({ children }: PropsWithChildren<{}>) => {
- const appThemeApi = useMemo(
- () => AppThemeSelector.createWithStorage(this.themes),
- [],
- );
-
- const { routePaths, routeParents, routeObjects } = useMemo(() => {
- const result = traverseElementTree({
- root: children,
- discoverers: [childDiscoverer, routeElementDiscoverer],
- collectors: {
- routePaths: routePathCollector,
- routeParents: routeParentCollector,
- routeObjects: routeObjectCollector,
- collectedPlugins: pluginCollector,
- },
- });
-
- validateRoutes(result.routePaths, result.routeParents);
-
- // TODO(Rugvip): Restructure the public API so that we can get an immediate view of
- // the app, rather than having to wait for the provider to render.
- // For now we need to push the additional plugins we find during
- // collection and then make sure we initialize things afterwards.
- result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
- this.verifyPlugins(this.plugins);
-
- // Initialize APIs once all plugins are available
- this.getApiHolder();
-
- return result;
- }, [children]);
-
- const loadedConfig = useConfigLoader(
- this.configLoader,
- this.components,
- appThemeApi,
- );
-
- const hasConfigApi = 'api' in loadedConfig;
- if (hasConfigApi) {
- const { api } = loadedConfig as { api: Config };
- this.configApi = api;
- }
-
- useEffect(() => {
- if (hasConfigApi) {
- const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!;
-
- for (const plugin of this.plugins.values()) {
- for (const output of plugin.output()) {
- switch (output.type) {
- case 'feature-flag': {
- featureFlagsApi.registerFlag({
- name: output.name,
- pluginId: plugin.getId(),
- });
- break;
- }
- default:
- break;
- }
- }
- }
- }
- }, [hasConfigApi, loadedConfig]);
-
- if ('node' in loadedConfig) {
- // Loading or error
- return loadedConfig.node;
- }
-
- return (
-
-
-
-
- {children}
-
-
-
-
- );
- };
- return Provider;
- }
-
- getRouter(): ComponentType<{}> {
- const {
- Router: RouterComponent,
- SignInPage: SignInPageComponent,
- } = this.components;
-
- // This wraps the sign-in page and waits for sign-in to be completed before rendering the app
- const SignInPageWrapper = ({
- component: Component,
- children,
- }: {
- component: ComponentType;
- children: ReactElement;
- }) => {
- const [result, setResult] = useState();
-
- if (result) {
- this.identityApi.setSignInResult(result);
- return children;
- }
-
- return ;
- };
-
- const AppRouter = ({ children }: PropsWithChildren<{}>) => {
- const configApi = useApi(configApiRef);
-
- let { pathname } = new URL(
- configApi.getOptionalString('app.baseUrl') ?? '/',
- 'http://dummy.dev', // baseUrl can be specified as just a path
- );
- if (pathname.endsWith('/')) {
- pathname = pathname.replace(/\/$/, '');
- }
-
- // If the app hasn't configured a sign-in page, we just continue as guest.
- if (!SignInPageComponent) {
- this.identityApi.setSignInResult({
- userId: 'guest',
- profile: {
- email: 'guest@example.com',
- displayName: 'Guest',
- },
- });
-
- return (
-
-
- {children}>} />
-
-
- );
- }
-
- return (
-
-
-
- {children}>} />
-
-
-
- );
- };
-
- return AppRouter;
- }
-
- private getApiHolder(): ApiHolder {
- if (this.apiHolder) {
- return this.apiHolder;
- }
-
- const registry = new ApiFactoryRegistry();
-
- registry.register('static', {
- api: appThemeApiRef,
- deps: {},
- factory: () => AppThemeSelector.createWithStorage(this.themes),
- });
- registry.register('static', {
- api: configApiRef,
- deps: {},
- factory: () => {
- if (!this.configApi) {
- throw new Error(
- 'Tried to access config API before config was loaded',
- );
- }
- return this.configApi;
- },
- });
- registry.register('static', {
- api: identityApiRef,
- deps: {},
- factory: () => this.identityApi,
- });
-
- // It's possible to replace the feature flag API, but since we must have at least
- // one implementation we add it here directly instead of through the defaultApis.
- registry.register('default', {
- api: featureFlagsApiRef,
- deps: {},
- factory: () => new LocalStorageFeatureFlags(),
- });
- for (const factory of this.defaultApis) {
- registry.register('default', factory);
- }
-
- for (const plugin of this.plugins) {
- for (const factory of plugin.getApis()) {
- if (!registry.register('default', factory)) {
- throw new Error(
- `Plugin ${plugin.getId()} tried to register duplicate or forbidden API factory for ${
- factory.api
- }`,
- );
- }
- }
- }
-
- for (const factory of this.apis) {
- if (!registry.register('app', factory)) {
- throw new Error(
- `Duplicate or forbidden API factory for ${factory.api} in app`,
- );
- }
- }
-
- ApiResolver.validateFactories(registry, registry.getAllApis());
-
- this.apiHolder = new ApiResolver(registry);
-
- return this.apiHolder;
- }
-
- /**
- * @deprecated
- */
- verify() {}
-
- private verifyPlugins(plugins: Iterable) {
- const pluginIds = new Set();
-
- for (const plugin of plugins) {
- const id = plugin.getId();
- if (pluginIds.has(id)) {
- throw new Error(`Duplicate plugin found '${id}'`);
- }
- pluginIds.add(id);
- }
- }
-}
diff --git a/packages/core-api/src/app/AppContext.test.tsx b/packages/core-api/src/app/AppContext.test.tsx
deleted file mode 100644
index 526b397130..0000000000
--- a/packages/core-api/src/app/AppContext.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 React, { useContext, Context } from 'react';
-import { renderHook } from '@testing-library/react-hooks';
-import { VersionedValue } from '../lib/versionedValues';
-import { getGlobalSingleton } from '../lib/globalObject';
-import { AppContext as AppContextV1 } from './types';
-import { AppContextProvider } from './AppContext';
-
-describe('v1 consumer', () => {
- const AppContext = getGlobalSingleton<
- Context>
- >('app-context');
-
- function useMockAppV1(): AppContextV1 {
- const impl = useContext(AppContext)?.atVersion(1);
- if (!impl) {
- throw new Error('no impl');
- }
- return impl;
- }
-
- it('should provide an app context', () => {
- const mockContext: AppContextV1 = {
- getComponents: jest.fn(),
- getSystemIcon: jest.fn(),
- getPlugins: jest.fn(),
- getProvider: jest.fn(),
- getRouter: jest.fn(),
- getRoutes: jest.fn(),
- };
-
- const renderedHook = renderHook(() => useMockAppV1(), {
- wrapper: ({ children }) => (
-
- ),
- });
- const result = renderedHook.result.current;
-
- expect(mockContext.getComponents).toHaveBeenCalledTimes(0);
- result.getComponents();
- expect(mockContext.getComponents).toHaveBeenCalledTimes(1);
-
- expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(0);
- result.getSystemIcon('icon');
- expect(mockContext.getSystemIcon).toHaveBeenCalledTimes(1);
- expect(mockContext.getSystemIcon).toHaveBeenCalledWith('icon');
-
- expect(mockContext.getPlugins).toHaveBeenCalledTimes(0);
- result.getPlugins();
- expect(mockContext.getPlugins).toHaveBeenCalledTimes(1);
-
- expect(mockContext.getProvider).toHaveBeenCalledTimes(0);
- result.getProvider();
- expect(mockContext.getProvider).toHaveBeenCalledTimes(1);
-
- expect(mockContext.getRouter).toHaveBeenCalledTimes(0);
- result.getRouter();
- expect(mockContext.getRouter).toHaveBeenCalledTimes(1);
-
- expect(mockContext.getRoutes).toHaveBeenCalledTimes(0);
- result.getRoutes();
- expect(mockContext.getRoutes).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/packages/core-api/src/app/AppContext.tsx b/packages/core-api/src/app/AppContext.tsx
deleted file mode 100644
index ab2d2d7861..0000000000
--- a/packages/core-api/src/app/AppContext.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 React, {
- createContext,
- PropsWithChildren,
- useContext,
- Context,
- useMemo,
-} from 'react';
-import {
- VersionedValue,
- createVersionedValueMap,
-} from '../lib/versionedValues';
-import {
- getGlobalSingleton,
- getOrCreateGlobalSingleton,
-} from '../lib/globalObject';
-import { AppContext as AppContextV1 } from './types';
-
-type AppContextType = VersionedValue<{ 1: AppContextV1 }> | undefined;
-const AppContext = getOrCreateGlobalSingleton('app-context', () =>
- createContext(undefined),
-);
-
-type Props = {
- appContext: AppContextV1;
-};
-
-export const AppContextProvider = ({
- appContext,
- children,
-}: PropsWithChildren) => {
- const versionedValue = useMemo(
- () => createVersionedValueMap({ 1: appContext }),
- [appContext],
- );
-
- return ;
-};
-
-export const useApp = (): AppContextV1 => {
- const versionedContext = useContext(
- getGlobalSingleton>('app-context'),
- );
- if (!versionedContext) {
- throw new Error('No app context available');
- }
- const appContext = versionedContext.atVersion(1);
- if (!appContext) {
- throw new Error('AppContext v1 not available');
- }
- return appContext;
-};
diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts
deleted file mode 100644
index 7dd87de392..0000000000
--- a/packages/core-api/src/app/AppIdentity.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { IdentityApi, ProfileInfo } from '../apis';
-import { SignInResult } from './types';
-
-/**
- * Implementation of the connection between the App-wide IdentityApi
- * and sign-in page.
- */
-export class AppIdentity implements IdentityApi {
- private hasIdentity = false;
- private userId?: string;
- private profile?: ProfileInfo;
- private idTokenFunc?: () => Promise;
- private signOutFunc?: () => Promise;
-
- getUserId(): string {
- if (!this.hasIdentity) {
- throw new Error(
- 'Tried to access IdentityApi userId before app was loaded',
- );
- }
- return this.userId!;
- }
-
- getProfile(): ProfileInfo {
- if (!this.hasIdentity) {
- throw new Error(
- 'Tried to access IdentityApi profile before app was loaded',
- );
- }
- return this.profile!;
- }
-
- async getIdToken(): Promise {
- if (!this.hasIdentity) {
- throw new Error(
- 'Tried to access IdentityApi idToken before app was loaded',
- );
- }
- return this.idTokenFunc?.();
- }
-
- async signOut(): Promise {
- if (!this.hasIdentity) {
- throw new Error(
- 'Tried to access IdentityApi signOutFunc before app was loaded',
- );
- }
- await this.signOutFunc?.();
- location.reload();
- }
-
- // This is indirectly called by the sign-in page to continue into the app.
- setSignInResult(result: SignInResult) {
- if (this.hasIdentity) {
- return;
- }
- if (!result.userId) {
- throw new Error('Invalid sign-in result, userId not set');
- }
- if (!result.profile) {
- throw new Error('Invalid sign-in result, profile not set');
- }
- this.hasIdentity = true;
- this.userId = result.userId;
- this.profile = result.profile;
- this.idTokenFunc = result.getIdToken;
- this.signOutFunc = result.signOut;
- }
-}
diff --git a/packages/core-api/src/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx
deleted file mode 100644
index b6a88e6fcc..0000000000
--- a/packages/core-api/src/app/AppThemeProvider.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 React, { useMemo, useEffect, useState, PropsWithChildren } from 'react';
-import { ThemeProvider, CssBaseline } from '@material-ui/core';
-import { useApi, appThemeApiRef, AppTheme } from '../apis';
-import { useObservable } from 'react-use';
-
-// This tries to find the most accurate match, but also falls back to less
-// accurate results in order to avoid errors.
-function resolveTheme(
- themeId: string | undefined,
- shouldPreferDark: boolean,
- themes: AppTheme[],
-) {
- if (themeId !== undefined) {
- const selectedTheme = themes.find(theme => theme.id === themeId);
- if (selectedTheme) {
- return selectedTheme;
- }
- }
-
- if (shouldPreferDark) {
- const darkTheme = themes.find(theme => theme.variant === 'dark');
- if (darkTheme) {
- return darkTheme;
- }
- }
-
- const lightTheme = themes.find(theme => theme.variant === 'light');
- if (lightTheme) {
- return lightTheme;
- }
-
- return themes[0];
-}
-
-const useShouldPreferDarkTheme = () => {
- const mediaQuery = useMemo(
- () => window.matchMedia('(prefers-color-scheme: dark)'),
- [],
- );
- const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches);
-
- useEffect(() => {
- const listener = (event: MediaQueryListEvent) => {
- setPrefersDark(event.matches);
- };
- mediaQuery.addListener(listener);
- return () => {
- mediaQuery.removeListener(listener);
- };
- }, [mediaQuery]);
-
- return shouldPreferDark;
-};
-
-export function AppThemeProvider({ children }: PropsWithChildren<{}>) {
- const appThemeApi = useApi(appThemeApiRef);
- const themeId = useObservable(
- appThemeApi.activeThemeId$(),
- appThemeApi.getActiveThemeId(),
- );
-
- // Browser feature detection won't change over time, so ignore lint rule
- const shouldPreferDark = Boolean(window.matchMedia)
- ? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks
- : false;
-
- const appTheme = resolveTheme(
- themeId,
- shouldPreferDark,
- appThemeApi.getInstalledThemes(),
- );
- if (!appTheme) {
- throw new Error('App has no themes');
- }
-
- return (
-
- {children}
-
- );
-}
diff --git a/packages/core-api/src/app/index.ts b/packages/core-api/src/app/index.ts
deleted file mode 100644
index 56e0800809..0000000000
--- a/packages/core-api/src/app/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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.
- */
-
-export { useApp } from './AppContext';
-export * from './types';
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
deleted file mode 100644
index a3eb6f423f..0000000000
--- a/packages/core-api/src/app/types.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright 2020 The Backstage Authors
- *
- * 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 { ComponentType } from 'react';
-import { IconComponent, IconComponentMap, IconKey } from '../icons/types';
-import { AnyExternalRoutes, BackstagePlugin } from '../plugin/types';
-import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing/types';
-import { AnyApiFactory } from '../apis/system';
-import { AppTheme, ProfileInfo } from '../apis/definitions';
-import { AppConfig } from '@backstage/config';
-
-export type BootErrorPageProps = {
- step: 'load-config' | 'load-chunk';
- error: Error;
-};
-
-export type SignInResult = {
- /**
- * User ID that will be returned by the IdentityApi
- */
- userId: string;
-
- profile: ProfileInfo;
-
- /**
- * Function used to retrieve an ID token for the signed in user.
- */
- getIdToken?: () => Promise;
-
- /**
- * Sign out handler that will be called if the user requests to sign out.
- */
- signOut?: () => Promise;
-};
-
-export type SignInPageProps = {
- /**
- * Set the sign-in result for the app. This should only be called once.
- */
- onResult(result: SignInResult): void;
-};
-
-export type AppComponents = {
- NotFoundErrorPage: ComponentType<{}>;
- BootErrorPage: ComponentType;
- Progress: ComponentType<{}>;
- Router: ComponentType<{}>;
-
- /**
- * An optional sign-in page that will be rendered instead of the AppRouter at startup.
- *
- * If a sign-in page is set, it will always be shown before the app, and it is up
- * to the sign-in page to handle e.g. saving of login methods for subsequent visits.
- *
- * The sign-in page will be displayed until it has passed up a result to the parent,
- * and which point the AppRouter and all of its children will be rendered instead.
- */
- SignInPage?: ComponentType;
-};
-
-/**
- * A function that loads in the App config that will be accessible via the ConfigApi.
- *
- * If multiple config objects are returned in the array, values in the earlier configs
- * will override later ones.
- */
-export type AppConfigLoader = () => Promise