From 6f954e3d985111cba1459a1824527917428f550f Mon Sep 17 00:00:00 2001 From: Michael Beckemeyer Date: Fri, 1 Dec 2023 10:54:48 +0100 Subject: [PATCH] Improve TypeScript Support (#25) --- .changeset/long-cups-drive.md | 53 +++++++++++++++ .changeset/smooth-cups-deny.md | 7 ++ .husky/pre-commit | 6 +- src/packages/http/index.ts | 13 +--- src/packages/integration/api.ts | 21 +++--- src/packages/runtime/CustomElement.test.ts | 30 +++++++++ src/packages/runtime/CustomElement.ts | 12 +--- src/packages/runtime/DeclaredService.test.ts | 48 ++++++++++++++ src/packages/runtime/DeclaredService.ts | 65 +++++++++++++++++++ src/packages/runtime/PropertiesRegistry.ts | 23 ------- src/packages/runtime/ServiceRegistry.ts | 31 --------- src/packages/runtime/api.ts | 16 ++--- .../ApplicationLifecycleEventService.ts | 4 +- src/packages/runtime/index.ts | 8 ++- .../ReactIntegration.test.ts | 56 +++++++++++----- .../runtime/react-integration/hooks.ts | 21 +++--- src/samples/api-sample/api-app/DemoUI.tsx | 3 +- .../extension-app/ActionsServiceImpl.ts | 6 +- .../extension-app/ActionsUI.tsx | 3 +- .../extension-sample/extension-app/api.ts | 13 +--- .../http-sample/http-app/HttpClient.ts | 7 +- src/samples/i18n-sample/i18n-app/I18nUI.tsx | 6 +- .../properties-app/AppUI.tsx | 4 +- .../properties-app/NotifierUI.tsx | 4 +- .../properties-sample/properties-app/api.ts | 12 ++-- src/types/pioneer-env.d.ts | 45 +++++++++++++ 26 files changed, 350 insertions(+), 167 deletions(-) create mode 100644 .changeset/long-cups-drive.md create mode 100644 .changeset/smooth-cups-deny.md create mode 100644 src/packages/runtime/DeclaredService.test.ts create mode 100644 src/packages/runtime/DeclaredService.ts delete mode 100644 src/packages/runtime/PropertiesRegistry.ts delete mode 100644 src/packages/runtime/ServiceRegistry.ts diff --git a/.changeset/long-cups-drive.md b/.changeset/long-cups-drive.md new file mode 100644 index 00000000..4406cc9e --- /dev/null +++ b/.changeset/long-cups-drive.md @@ -0,0 +1,53 @@ +--- +"@open-pioneer/runtime": major +--- + +**Breaking Change**: change how services integrate into TypeScript (fixes #22). +The old TypeScript integration had unexpected edge cases, see the linked issue. + +NOTE: The changes below have no impact on runtime behavior, but they may trigger TypeScript errors in your code. + +- To register a service's type with TypeScript, one previously used a block such as this: + + ```ts + // OLD! Can be removed + import "@open-pioneer/runtime"; + declare module "@open-pioneer/runtime" { + interface ServiceRegistry { + "http.HttpService": HttpService; + } + } + ``` + + The new method requires the developer to change the service's declaration. + Simply add `extends DeclaredService<"SERVICE_ID">` to your service interface, where `SERVICE_ID` should match the service's interface name (`"provides"` in `build.config.mjs`). + + ```diff + + import { DeclaredService } from "@open-pioneer/runtime"; + + - export interface HttpService { + + export interface HttpService extends DeclaredService<"http.HttpService"> { + + - import "@open-pioneer/runtime"; + - declare module "@open-pioneer/runtime" { + - interface ServiceRegistry { + - "http.HttpService": HttpService; + - } + - } + ``` + +- To use a service from React code (i.e. `useService` and `useServices`), you must now use the explicit service type in the hook's generic parameter list. Otherwise the hook will simply return `unknown`: + + ```diff + + import { HttpService } from "@open-pioneer/http"; + - const httpService = useService("http.HttpService"); + + const httpService = useService("http.HttpService"); + ``` + + This change was necessary to fix an issue where the global registration of the service interface (and its association with the string constant) was not available. + + The system will still check that the provided string matches the string constant used in the service's declaration (`DeclaredService<...>`), so type safety is preserved. + +- The types `InterfaceName` and `ServiceType` have been removed. Use explicit service interfaces instead. +- The interfaces `ServiceRegistry` and `PropertiesRegistry` have been removed as global registration is no longer possible. +- The type `RawApplicationProperties` has been removed. Use `ApplicationProperties` instead. diff --git a/.changeset/smooth-cups-deny.md b/.changeset/smooth-cups-deny.md new file mode 100644 index 00000000..92e030e8 --- /dev/null +++ b/.changeset/smooth-cups-deny.md @@ -0,0 +1,7 @@ +--- +"@open-pioneer/integration": major +"@open-pioneer/http": major +"@open-pioneer/base-theme": major +--- + +Compatibility with @open-pioneer/runtime@^2 diff --git a/.husky/pre-commit b/.husky/pre-commit index 1b6a53c7..daf0da48 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -9,12 +9,12 @@ fi echo '--- checking for consistent dependencies across packages' pnpm lint-shared-versions -echo '--- run linting --- ' -pnpm lint - echo '--- run prettier ---' pnpm prettier-check +echo '--- run linting --- ' +pnpm lint + echo '--- run typescript check ---' pnpm check-types diff --git a/src/packages/http/index.ts b/src/packages/http/index.ts index 688f2086..701efdd8 100644 --- a/src/packages/http/index.ts +++ b/src/packages/http/index.ts @@ -1,12 +1,13 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 +import { DeclaredService } from "@open-pioneer/runtime"; /** * Central service for sending HTTP requests. * * Use the interface `"http.HttpService"` to obtain an instance of this service. */ -export interface HttpService { +export interface HttpService extends DeclaredService<"http.HttpService"> { /** * Requests the given `resource` via HTTP and returns the response. * @@ -20,13 +21,3 @@ export interface HttpService { */ fetch(resource: RequestInfo | URL, init?: RequestInit): Promise; } - -import "@open-pioneer/runtime"; -declare module "@open-pioneer/runtime" { - interface ServiceRegistry { - "http.HttpService": HttpService; - } -} - -// Get rid of empty chunk warning -export default undefined; diff --git a/src/packages/integration/api.ts b/src/packages/integration/api.ts index aa05d5b6..15b79ff9 100644 --- a/src/packages/integration/api.ts +++ b/src/packages/integration/api.ts @@ -1,13 +1,20 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 -import { type ApiExtension, type ApiMethods, type ApiMethod } from "@open-pioneer/runtime"; +import { + type ApiExtension, + type ApiMethods, + type ApiMethod, + DeclaredService +} from "@open-pioneer/runtime"; + +export { ApiExtension, ApiMethod, ApiMethods }; // re-export for consistency /** * Emits events to users of the current web component. * * Use the interface `"integration.ExternalEventService"` to obtain an instance of this service. */ -export interface ExternalEventService { +export interface ExternalEventService extends DeclaredService<"integration.ExternalEventService"> { /** * Emits an event to the host site as a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). * @@ -45,13 +52,3 @@ export interface ExternalEventService { */ emitEvent(event: Event): void; } - -export { ApiExtension, ApiMethod, ApiMethods }; // re-export for consistency - -import "@open-pioneer/runtime"; -declare module "@open-pioneer/runtime" { - interface ServiceRegistry { - "integration.ApiExtension": ApiExtension; - "integration.ExternalEventService": ExternalEventService; - } -} diff --git a/src/packages/runtime/CustomElement.test.ts b/src/packages/runtime/CustomElement.test.ts index 09d4345c..6f794c26 100644 --- a/src/packages/runtime/CustomElement.test.ts +++ b/src/packages/runtime/CustomElement.test.ts @@ -26,6 +26,10 @@ interface InternalElementType extends ApplicationElement { $inspectElementState?(): any; } +afterEach(() => { + vi.restoreAllMocks(); +}); + describe("simple rendering", function () { const SIMPLE_STYLE = ".test { color: red }"; const SIMPLE_ELEM = createCustomElement({ @@ -346,6 +350,8 @@ describe("application lifecycle events", function () { }); it("does not signal 'before-stop' when start fails", async function () { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + const events: string[] = []; class Listener implements ApplicationLifecycleListener { afterApplicationStart() { @@ -391,6 +397,30 @@ describe("application lifecycle events", function () { }); expect(events).toEqual([]); + expect(errorSpy).toMatchInlineSnapshot(` + [MockFunction error] { + "calls": [ + [ + "#1", + [Error: help!], + ], + [ + "#2", + [Error: runtime:config-resolution-failed: Failed to resolve application properties.], + ], + ], + "results": [ + { + "type": "return", + "value": undefined, + }, + { + "type": "return", + "value": undefined, + }, + ], + } + `); }); }); diff --git a/src/packages/runtime/CustomElement.ts b/src/packages/runtime/CustomElement.ts index fd4a5ee1..e5431693 100644 --- a/src/packages/runtime/CustomElement.ts +++ b/src/packages/runtime/CustomElement.ts @@ -26,7 +26,6 @@ import { RUNTIME_AUTO_START } from "./builtin-services"; import { ReferenceSpec } from "./service-layer/InterfaceSpec"; -import { PropertiesRegistry } from "./PropertiesRegistry"; import { AppI18n, initI18n } from "./i18n"; import { ApplicationLifecycleEventService } from "./builtin-services/ApplicationLifecycleEventService"; const LOG = createLogger("runtime:CustomElement"); @@ -108,8 +107,10 @@ export interface ApplicationConfig { /** * Allows the application to override default properties in all packages. + * + * Properties are typed when the package contains type definitions for them. */ -export interface RawApplicationProperties { +export interface ApplicationProperties { /** * Key: the name of the package. * Value: A record of configuration properties (key/value pairs). @@ -119,13 +120,6 @@ export interface RawApplicationProperties { [packageName: string]: Record; } -/** - * Allows the application to override default properties in all packages. - * - * Properties are typed when the package contains type definitions for them. - */ -export type ApplicationProperties = RawApplicationProperties & Partial; - /** * The interface implemented by web components produced via {@link createCustomElement}. */ diff --git a/src/packages/runtime/DeclaredService.test.ts b/src/packages/runtime/DeclaredService.test.ts new file mode 100644 index 00000000..e26700b8 --- /dev/null +++ b/src/packages/runtime/DeclaredService.test.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +/* eslint-disable unused-imports/no-unused-vars */ +import { DeclaredService, InterfaceNameForServiceType } from "./DeclaredService"; +import { it } from "vitest"; + +// Tests are on type level only +it("dummy test to allow a file without any real tests", () => undefined); + +/** + * Returns type `true` if types A and B are equal (type false otherwise). + * See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 + */ +// prettier-ignore +type Equal = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? true : false; + +/** + * Returns type `true` if T is any kind of string, `false` otherwise. + */ +type IsString = T extends string ? true : false; + +// Expect all strings are allowed when service type is unknown +{ + type IFace = InterfaceNameForServiceType; + const isString: Equal = true; +} + +// Expect only the declared interface name is allowed when an explicit service is provided +{ + interface MyService extends DeclaredService<"my.service"> { + foo(): void; + } + + type IFace = InterfaceNameForServiceType; + const isConstant: Equal = true; +} + +// Expect an error is returned when an explicit type is used that does not extend DeclaredService +{ + interface MyService { + foo(): void; + } + + type IFace = InterfaceNameForServiceType; + const isString: IsString = false; +} diff --git a/src/packages/runtime/DeclaredService.ts b/src/packages/runtime/DeclaredService.ts new file mode 100644 index 00000000..21e77575 --- /dev/null +++ b/src/packages/runtime/DeclaredService.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 +declare const INTERNAL_ASSOCIATED_SERVICE_METADATA: unique symbol; +declare const ERROR: unique symbol; + +/** + * Base interface for services that are associated with a well known interface name. + * + * By using this base interface, you can ensure that users of your interface use the correct interface name. + * + * @example + * ```ts + * // MyLogger should be referenced via "my-package.Logger" + * export interface MyLogger extends DeclaredService<"my-package.Logger"> { + * log(message: string): void; + * } + * ``` + * + * > Note: TypeScript may list the `INTERNAL_ASSOCIATED_SERVICE_METADATA` property + * > when generating the implementation for an interface extending this type. + * > You can simply remove the offending line; it is not required (and not possible) + * > to implement that attribute - it only exists for the compiler. + */ +export interface DeclaredService { + /** + * Internal type-level service metadata. + * + * Note: there is no need to implement this symbol attribute. + * It is optional and only exists for the compiler, never at runtime. + * + * @internal + */ + [INTERNAL_ASSOCIATED_SERVICE_METADATA]?: ServiceMetadata; +} + +/** + * Given a type implementing {@link DeclaredService}, this type will produce the interface name associated with the service type. + */ +export type AssociatedInterfaceName> = T extends DeclaredService< + infer InterfaceName +> + ? InterfaceName + : never; + +/** + * This helper type produces the expected `interfaceName` (a string parameter) for the given service type. + * + * 1. If `ServiceType` is `unknown`, it will produce `string` to allow arbitrary parameters. + * 2. If `ServiceType` implements {@link DeclaredService}, it will enforce the associated interface name. + * 3. Otherwise, a compile time error is generated. + */ +export type InterfaceNameForServiceType = unknown extends ServiceType + ? string + : ServiceType extends DeclaredService + ? AssociatedInterfaceName + : { + [ERROR]: "TypeScript integration was not set up properly for this service. Make sure the service's TypeScript interface extends 'DeclaredService'."; + }; + +/** + * @internal + */ +interface ServiceMetadata { + interfaceName: InterfaceName; +} diff --git a/src/packages/runtime/PropertiesRegistry.ts b/src/packages/runtime/PropertiesRegistry.ts deleted file mode 100644 index b0ffd594..00000000 --- a/src/packages/runtime/PropertiesRegistry.ts +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) -// SPDX-License-Identifier: Apache-2.0 - -/** - * Maps a package name to its supported properties. - * - * By default, properties for all packages are untyped (as Record). - * - * @example - * - * ```ts - * import "@open-pioneer/runtime"; - * declare module "@open-pioneer/runtime" { - * interface PropertiesRegistry { - * // Declares that properties for the "logging" package must - * // conform to the type on the right when overridden by an app. - * "logging": Partial - * } - * } - * ``` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PropertiesRegistry {} diff --git a/src/packages/runtime/ServiceRegistry.ts b/src/packages/runtime/ServiceRegistry.ts deleted file mode 100644 index 606297e4..00000000 --- a/src/packages/runtime/ServiceRegistry.ts +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) -// SPDX-License-Identifier: Apache-2.0 - -/** - * Maps a registered interface name to a service type. - * The interface can be reopened by client packages to add additional registrations. - * - * @example - * - * ```ts - * import "@open-pioneer/runtime"; - * declare module "@open-pioneer/runtime" { - * interface ServiceRegistry { - * // Associates the interface name with the TypeScript interface - * "logging.LogService": Logger; - * } - * } - * ``` - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ServiceRegistry {} - -/** - * A well known interface name registered with the {@link ServiceRegistry}. - */ -export type InterfaceName = keyof ServiceRegistry; - -/** - * Returns the registered service type for the given interface name. - */ -export type ServiceType = ServiceRegistry[I]; diff --git a/src/packages/runtime/api.ts b/src/packages/runtime/api.ts index 7a8370e0..9efc5ef9 100644 --- a/src/packages/runtime/api.ts +++ b/src/packages/runtime/api.ts @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 +import { DeclaredService } from "./DeclaredService"; /* eslint-disable @typescript-eslint/no-explicit-any */ export type ApiMethod = (...args: any[]) => any; @@ -23,7 +24,7 @@ export interface ApiExtension { * A service provided by the system. * Used by the runtime to assemble the public facing API. */ -export interface ApiService { +export interface ApiService extends DeclaredService<"runtime.ApiService"> { /** * Called by the runtime to gather methods that should be available from the web component's API. */ @@ -33,7 +34,7 @@ export interface ApiService { /** * A service provided by the system, useful for accessing values that are global to the application. */ -export interface ApplicationContext { +export interface ApplicationContext extends DeclaredService<"runtime.ApplicationContext"> { /** * The web component's host element. * This dom node can be accessed by the host site. @@ -73,7 +74,8 @@ export interface ApplicationContext { * **Experimental**. This interface is not affected by semver guarantees. * It may change (or be removed) in a future minor release. */ -export interface ApplicationLifecycleListener { +export interface ApplicationLifecycleListener + extends DeclaredService<"runtime.ApplicationLifecycleListener"> { /** * Called after all services required by the application have been started. */ @@ -84,11 +86,3 @@ export interface ApplicationLifecycleListener { */ beforeApplicationStop?(): void; } - -declare module "./ServiceRegistry" { - interface ServiceRegistry { - "runtime.ApiService": ApiService; - "runtime.ApplicationContext": ApplicationContext; - "runtime.ApplicationLifecycleListener": ApplicationLifecycleListener; - } -} diff --git a/src/packages/runtime/builtin-services/ApplicationLifecycleEventService.ts b/src/packages/runtime/builtin-services/ApplicationLifecycleEventService.ts index 66842b67..e1c41b43 100644 --- a/src/packages/runtime/builtin-services/ApplicationLifecycleEventService.ts +++ b/src/packages/runtime/builtin-services/ApplicationLifecycleEventService.ts @@ -1,17 +1,15 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 - import { createLogger } from "@open-pioneer/core"; import { Error } from "@open-pioneer/core"; import { ServiceOptions } from "../Service"; -import { ServiceType } from "../ServiceRegistry"; import { ApplicationLifecycleListener } from "../api"; import { ErrorId } from "../errors"; const LOG = createLogger("runtime:ApplicationLifecycleEventService"); interface References { - listeners: ServiceType<"runtime.ApplicationLifecycleListener">[]; + listeners: ApplicationLifecycleListener[]; } export class ApplicationLifecycleEventService { diff --git a/src/packages/runtime/index.ts b/src/packages/runtime/index.ts index e851f410..b9ed037e 100644 --- a/src/packages/runtime/index.ts +++ b/src/packages/runtime/index.ts @@ -9,9 +9,11 @@ export { type ApplicationConfig, type CustomElementOptions, type ConfigContext, - type RawApplicationProperties, createCustomElement } from "./CustomElement"; export * from "./Service"; -export * from "./ServiceRegistry"; -export * from "./PropertiesRegistry"; +export { + type DeclaredService, + type AssociatedInterfaceName, + type InterfaceNameForServiceType +} from "./DeclaredService"; diff --git a/src/packages/runtime/react-integration/ReactIntegration.test.ts b/src/packages/runtime/react-integration/ReactIntegration.test.ts index 9e61ddb0..dbc81ce7 100644 --- a/src/packages/runtime/react-integration/ReactIntegration.test.ts +++ b/src/packages/runtime/react-integration/ReactIntegration.test.ts @@ -3,21 +3,22 @@ /** * @vitest-environment happy-dom */ -import { createElement } from "react"; -import { beforeEach, expect, it } from "vitest"; -import { usePropertiesInternal, useServiceInternal, useServicesInternal } from "./hooks"; import { findByTestId, findByText } from "@testing-library/dom"; import { act } from "@testing-library/react"; +import { createElement } from "react"; +import { beforeEach, expect, it, SpyInstance, afterEach, vi } from "vitest"; import { Service, ServiceConstructor } from "../Service"; -// eslint-disable-next-line import/no-relative-packages -import { UIWithProperties, UIWithService, UIWithServices } from "./test-data/test-package/UI"; +import { usePropertiesInternal, useServiceInternal, useServicesInternal } from "./hooks"; +import { useTheme } from "@open-pioneer/chakra-integration"; +import { PackageIntl, createEmptyI18n } from "../i18n"; +import { InterfaceSpec, ReferenceSpec } from "../service-layer/InterfaceSpec"; +import { PackageRepr } from "../service-layer/PackageRepr"; import { ServiceLayer } from "../service-layer/ServiceLayer"; +import { ServiceRepr, createConstructorFactory } from "../service-layer/ServiceRepr"; import { ReactIntegration } from "./ReactIntegration"; -import { PackageRepr } from "../service-layer/PackageRepr"; -import { createConstructorFactory, ServiceRepr } from "../service-layer/ServiceRepr"; -import { InterfaceSpec, ReferenceSpec } from "../service-layer/InterfaceSpec"; -import { createEmptyI18n, PackageIntl } from "../i18n"; -import { useTheme } from "@open-pioneer/chakra-integration"; + +// eslint-disable-next-line import/no-relative-packages +import { UIWithProperties, UIWithService, UIWithServices } from "./test-data/test-package/UI"; interface TestProvider { value: string; @@ -27,9 +28,18 @@ beforeEach(() => { document.body.innerHTML = ""; }); +let errorSpy!: SpyInstance; +beforeEach(() => { + errorSpy = vi.spyOn(console, "error"); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + it("should allow access to service via react hook", async () => { function TestComponent() { - const service = useServiceInternal("test", "test.Provider") as TestProvider; + const service = useServiceInternal("test", "test.Provider") as TestProvider; return createElement("span", undefined, `Hello ${service.value}`); } @@ -55,8 +65,10 @@ it("should allow access to service via react hook", async () => { }); it("should get error when using undefined service", async () => { + errorSpy.mockImplementation(doNothing); + function TestComponent() { - const service = useServiceInternal("test", "test.Provider") as TestProvider; + const service = useServiceInternal("test", "test.Provider") as TestProvider; return createElement("span", undefined, `Hello ${service.value}`); } @@ -69,11 +81,12 @@ it("should get error when using undefined service", async () => { integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); + expect(errorSpy).toHaveBeenCalledOnce(); }); it("should allow access to service with qualifier via react hook", async () => { function TestComponent() { - const service = useServiceInternal("test", "test.Provider", { + const service = useServiceInternal("test", "test.Provider", { qualifier: "foo" }) as TestProvider; return createElement("span", undefined, `Hello ${service.value}`); @@ -101,8 +114,10 @@ it("should allow access to service with qualifier via react hook", async () => { }); it("should deny access to service when the qualifier does not match", async () => { + errorSpy.mockImplementation(doNothing); + function TestComponent() { - const service = useServiceInternal("test", "test.Provider", { + const service = useServiceInternal("test", "test.Provider", { qualifier: "bar" }) as TestProvider; return createElement("span", undefined, `Hello ${service.value}`); @@ -126,11 +141,12 @@ it("should deny access to service when the qualifier does not match", async () = integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); + expect(errorSpy).toHaveBeenCalledOnce(); }); it("should allow access to all services via react hook", async () => { function TestComponent() { - const services = useServicesInternal("test", "test.Provider") as TestProvider[]; + const services = useServicesInternal("test", "test.Provider") as TestProvider[]; return createElement( "span", undefined, @@ -174,8 +190,10 @@ it("should allow access to all services via react hook", async () => { }); it("should deny access to all services if declaration is missing", async () => { + errorSpy.mockImplementation(doNothing); + function TestComponent() { - const services = useServicesInternal("test", "test.Provider") as TestProvider[]; + const services = useServicesInternal("test", "test.Provider") as TestProvider[]; return createElement( "span", undefined, @@ -192,6 +210,7 @@ it("should deny access to all services if declaration is missing", async () => { integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); + expect(errorSpy).toHaveBeenCalledOnce(); }); it("should be able to read properties from react component", async () => { @@ -287,6 +306,8 @@ it("should provide the autogenerated useProperties hook", async () => { }); it("should throw error when requesting properties from an unknown package", async () => { + errorSpy.mockImplementation(doNothing); + const { integration } = createIntegration({ disablePackage: true }); @@ -301,6 +322,7 @@ it("should throw error when requesting properties from an unknown package", asyn integration.render(TestComponent); }); }).toThrowErrorMatchingSnapshot(); + expect(errorSpy).toHaveBeenCalledOnce(); }); it("should apply the configured chakra theme", async () => { @@ -393,3 +415,5 @@ function createIntegration(options?: { }); return { integration, wrapper }; } + +function doNothing() {} diff --git a/src/packages/runtime/react-integration/hooks.ts b/src/packages/runtime/react-integration/hooks.ts index 1c5ba529..33cb13aa 100644 --- a/src/packages/runtime/react-integration/hooks.ts +++ b/src/packages/runtime/react-integration/hooks.ts @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { useContext, useMemo } from "react"; import { Error } from "@open-pioneer/core"; -import { InterfaceName, ServiceType } from "../ServiceRegistry"; import { PackageContext as PackageContext, PackageContextMethods } from "@open-pioneer/runtime-react-support"; import { ErrorId } from "../errors"; import { PackageIntl } from "../i18n"; +import { InterfaceNameForServiceType } from "../DeclaredService"; /* @@ -41,16 +41,11 @@ export interface UseServiceOptions { * * @private */ -export function useServiceInternal( +export function useServiceInternal( packageName: string, - interfaceName: Interface, + interfaceName: InterfaceNameForServiceType, options?: UseServiceOptions -): ServiceType; -export function useServiceInternal( - packageName: string, - interfaceName: string, - options?: UseServiceOptions -): unknown; +): ServiceType; export function useServiceInternal( packageName: string, interfaceName: string, @@ -78,11 +73,11 @@ export function useServiceInternal( * * @private */ -export function useServicesInternal( +export function useServicesInternal( packageName: string, - interfaceName: Interface -): ServiceType[]; -export function useServicesInternal(packageName: string, interfaceName: string): unknown[]; + interfaceName: InterfaceNameForServiceType, + options?: UseServiceOptions +): ServiceType[]; export function useServicesInternal(packageName: string, interfaceName: string): unknown[] { const context = useContext(PackageContext); const services = useMemo( diff --git a/src/samples/api-sample/api-app/DemoUI.tsx b/src/samples/api-sample/api-app/DemoUI.tsx index 39fb6e5d..f0ce873c 100644 --- a/src/samples/api-sample/api-app/DemoUI.tsx +++ b/src/samples/api-sample/api-app/DemoUI.tsx @@ -4,9 +4,10 @@ import { useService } from "open-pioneer:react-hooks"; import { TextService } from "./TextService"; import { Button, Container, VStack, Text, Heading } from "@open-pioneer/chakra-integration"; import { useEffect, useState } from "react"; +import { ExternalEventService } from "@open-pioneer/integration"; export function DemoUI() { - const eventService = useService("integration.ExternalEventService"); + const eventService = useService("integration.ExternalEventService"); const emitEvent = () => { eventService.emitEvent("my-custom-event", { data: "my-event-data" diff --git a/src/samples/extension-sample/extension-app/ActionsServiceImpl.ts b/src/samples/extension-sample/extension-app/ActionsServiceImpl.ts index 1c7d19cd..778fe65a 100644 --- a/src/samples/extension-sample/extension-app/ActionsServiceImpl.ts +++ b/src/samples/extension-sample/extension-app/ActionsServiceImpl.ts @@ -1,10 +1,10 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 -import { Service, ServiceOptions, ServiceType } from "@open-pioneer/runtime"; -import { Action, ActionService } from "./api"; +import { Service, ServiceOptions } from "@open-pioneer/runtime"; +import { Action, ActionProvider, ActionService } from "./api"; interface References { - providers: ServiceType<"extension-app.ActionProvider">[]; + providers: ActionProvider[]; } export class ActionServiceImpl implements Service { diff --git a/src/samples/extension-sample/extension-app/ActionsUI.tsx b/src/samples/extension-sample/extension-app/ActionsUI.tsx index fc81cb2d..0f77c75e 100644 --- a/src/samples/extension-sample/extension-app/ActionsUI.tsx +++ b/src/samples/extension-sample/extension-app/ActionsUI.tsx @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Button, Container, Heading, Text, VStack } from "@open-pioneer/chakra-integration"; import { useService } from "open-pioneer:react-hooks"; +import { ActionService } from "./api"; export function ActionsUI() { - const service = useService("extension-app.ActionService"); + const service = useService("extension-app.ActionService"); const buttons = service.getActionInfo().map(({ id, text }) => (