diff --git a/.changeset/funny-rocks-train.md b/.changeset/funny-rocks-train.md new file mode 100644 index 0000000000..f5b2fbba63 --- /dev/null +++ b/.changeset/funny-rocks-train.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Internal refactor of usage of opaque types. diff --git a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts index 840e45ff4a..a77ffd485b 100644 --- a/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts +++ b/packages/frontend-internal/src/wiring/InternalExtensionDefinition.ts @@ -25,19 +25,19 @@ import { PortableSchema, ResolvedExtensionInputs, } from '@backstage/frontend-plugin-api'; +import { OpaqueType } from './OpaqueType'; -export type InternalExtensionDefinition< - T extends ExtensionDefinitionParameters = ExtensionDefinitionParameters, -> = ExtensionDefinition & { - readonly kind?: string; - readonly namespace?: string; - readonly name?: string; - readonly attachTo: { id: string; input: string }; - readonly disabled: boolean; - readonly configSchema?: PortableSchema; -} & ( +export const OpaqueExtensionDefinition = OpaqueType.create<{ + public: ExtensionDefinition; + versions: | { readonly version: 'v1'; + readonly kind?: string; + readonly namespace?: string; + readonly name?: string; + readonly attachTo: { id: string; input: string }; + readonly disabled: boolean; + readonly configSchema?: PortableSchema; readonly inputs: { [inputName in string]: { $$type: '@backstage/ExtensionInput'; @@ -63,6 +63,12 @@ export type InternalExtensionDefinition< } | { readonly version: 'v2'; + readonly kind?: string; + readonly namespace?: string; + readonly name?: string; + readonly attachTo: { id: string; input: string }; + readonly disabled: boolean; + readonly configSchema?: PortableSchema; readonly inputs: { [inputName in string]: ExtensionInput< AnyExtensionDataRef, @@ -81,24 +87,8 @@ export type InternalExtensionDefinition< >; }>; }): Iterable>; - } - ); - -/** @internal */ -export function toInternalExtensionDefinition< - T extends ExtensionDefinitionParameters, ->(overrides: ExtensionDefinition): InternalExtensionDefinition { - const internal = overrides as InternalExtensionDefinition; - if (internal.$$type !== '@backstage/ExtensionDefinition') { - throw new Error( - `Invalid extension definition instance, bad type '${internal.$$type}'`, - ); - } - const version = internal.version; - if (version !== 'v1' && version !== 'v2') { - throw new Error( - `Invalid extension definition instance, bad version '${version}'`, - ); - } - return internal; -} + }; +}>({ + type: '@backstage/ExtensionDefinition', + versions: ['v1', 'v2'], +}); diff --git a/packages/frontend-internal/src/wiring/OpaqueType.test.ts b/packages/frontend-internal/src/wiring/OpaqueType.test.ts new file mode 100644 index 0000000000..2e49ca9029 --- /dev/null +++ b/packages/frontend-internal/src/wiring/OpaqueType.test.ts @@ -0,0 +1,416 @@ +/* + * Copyright 2024 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 { OpaqueType } from './OpaqueType'; + +describe('OpaqueType', () => { + it('should create a basic opaque type with a single version', () => { + type MyType = { + $$type: 'my-type'; + }; + + const OpaqueMyType = OpaqueType.create<{ + public: MyType; + versions: { + version: 'v1'; + foo: string; + }; + }>({ + type: 'my-type', + versions: ['v1'], + }); + + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v2', { + foo: 'bar', + }); + + // @ts-expect-error - missing internal field + OpaqueMyType.createInstance('v1', {}); + + OpaqueMyType.createInstance('v1', { + // @ts-expect-error - invalid internal field + foo: 3, + }); + + const myInstance = OpaqueMyType.createInstance('v1', { + foo: 'bar', + }); + + expect(myInstance.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstance.version).toBe('v1'); + // @ts-expect-error - internal field not accessible + expect(myInstance.foo).toBe('bar'); + + expect(OpaqueMyType.isType(myInstance)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); + expect(OpaqueMyType.isType({ $$type: 'some-other' })).toBe(false); + expect(OpaqueMyType.isType({ $$type: 'my-type' })).toBe(true); + + const myInternal = OpaqueMyType.toInternal(myInstance); + expect(myInternal).toBe(myInstance); + // All fields accessible + expect(myInternal.$$type).toBe('my-type'); + expect(myInternal.version).toBe('v1'); + expect(myInternal.foo).toBe('bar'); + + expect(() => + OpaqueMyType.toInternal('hello'), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => OpaqueMyType.toInternal(3)).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => + OpaqueMyType.toInternal(undefined), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => + OpaqueMyType.toInternal(Symbol('wat')), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => + OpaqueMyType.toInternal(null), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => + OpaqueMyType.toInternal(() => {}), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got ''"`, + ); + expect(() => + OpaqueMyType.toInternal({ $$type: 'some-other-type' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got 'some-other-type'"`, + ); + expect(() => + OpaqueMyType.toInternal({ an: 'object' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type, expected 'my-type', but got '[object Object]'"`, + ); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version undefined, expected 'v1'"`, + ); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type', version: 'v0' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version 'v0', expected 'v1'"`, + ); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type', version: { foo: 'bar' } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version '[object Object]', expected 'v1'"`, + ); + }); + + it('should create a basic opaque type with multiple versions', () => { + type MyType = { + $$type: 'my-type'; + }; + + const OpaqueMyType = OpaqueType.create<{ + public: MyType; + versions: + | { + version: 'v1'; + foo: string; + } + | { + version: 'v2'; + bar: string; + } + | { + version: 'v3'; + baz: string; + }; + }>({ + type: 'my-type', + versions: ['v1', 'v2', 'v3'], + }); + + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v0', { + foo: 'bar', + }); + + // @ts-expect-error - missing internal field + OpaqueMyType.createInstance('v1', {}); + + OpaqueMyType.createInstance('v1', { + // @ts-expect-error - invalid internal field + foo: 3, + }); + + OpaqueMyType.createInstance('v2', { + // @ts-expect-error - version mismatch + foo: 'bar', + }); + + OpaqueMyType.createInstance('v1', { + // @ts-expect-error - version mismatch + bar: 'foo', + }); + + const myInstanceV1 = OpaqueMyType.createInstance('v1', { + foo: 'bar', + }); + + const myInstanceV2 = OpaqueMyType.createInstance('v2', { + bar: 'foo', + }); + + expect(myInstanceV1.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstanceV1.version).toBe('v1'); + // @ts-expect-error - internal field not accessible + expect(myInstanceV1.foo).toBe('bar'); + + expect(myInstanceV2.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstanceV2.version).toBe('v2'); + // @ts-expect-error - internal field not accessible + expect(myInstanceV2.bar).toBe('foo'); + + expect(OpaqueMyType.isType(myInstanceV1)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); + + const myInternalV1 = OpaqueMyType.toInternal(myInstanceV1); + expect(myInternalV1).toBe(myInstanceV1); + // All fields accessible + expect(myInternalV1.$$type).toBe('my-type'); + expect(myInternalV1.version).toBe('v1'); + // @ts-expect-error - version has not been narrowed down + expect(myInternalV1.foo).toBe('bar'); + + const myInternalV2 = OpaqueMyType.toInternal(myInstanceV2); + expect(myInternalV2).toBe(myInstanceV2); + // All fields accessible + expect(myInternalV2.$$type).toBe('my-type'); + expect(myInternalV2.version).toBe('v2'); + // @ts-expect-error - version has not been narrowed down + expect(myInternalV2.bar).toBe('foo'); + + // Narrowing the version allows access to internal fields + expect(myInternalV1.version === 'v1' && myInternalV1.foo).toBe('bar'); + expect(myInternalV2.version === 'v2' && myInternalV2.bar).toBe('foo'); + + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version undefined, expected 'v1', 'v2', or 'v3'"`, + ); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type', version: 'v0' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version 'v0', expected 'v1', 'v2', or 'v3'"`, + ); + }); + + it('should support undefined version for backwards compatibility', () => { + type MyType = { + $$type: 'my-type'; + }; + + const OpaqueMyType = OpaqueType.create<{ + public: MyType; + versions: { + version: undefined; + foo: string; + }; + }>({ + type: 'my-type', + versions: [undefined], + }); + + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v1', { + foo: 'bar', + }); + + // @ts-expect-error - missing internal field + OpaqueMyType.createInstance(undefined, {}); + + OpaqueMyType.createInstance(undefined, { + // @ts-expect-error - invalid internal field + foo: 3, + }); + + const myInstance = OpaqueMyType.createInstance(undefined, { + foo: 'bar', + }); + + expect(myInstance.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstance.version).toBe(undefined); + // @ts-expect-error - internal field not accessible + expect(myInstance.foo).toBe('bar'); + + expect(OpaqueMyType.isType(myInstance)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); + + const myInternal = OpaqueMyType.toInternal(myInstance); + expect(myInternal).toBe(myInstance); + // All fields accessible + expect(myInternal.$$type).toBe('my-type'); + expect(myInternal.version).toBe(undefined); + expect(myInternal.foo).toBe('bar'); + + expect(OpaqueMyType.toInternal({ $$type: 'my-type' })).toBeDefined(); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type', version: 'v0' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version 'v0', expected undefined"`, + ); + }); + + it('should support undefined version mixed with defined versions', () => { + type MyType = { + $$type: 'my-type'; + }; + + const OpaqueMyType = OpaqueType.create<{ + public: MyType; + versions: + | { + version: 'v1'; + foo: string; + } + | { + version: undefined; + bar: string; + }; + }>({ + type: 'my-type', + versions: [undefined, 'v1'], + }); + + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v0', { + foo: 'bar', + }); + + // @ts-expect-error - missing internal field + OpaqueMyType.createInstance('v1', {}); + + OpaqueMyType.createInstance('v1', { + // @ts-expect-error - invalid internal field + foo: 3, + }); + + OpaqueMyType.createInstance(undefined, { + // @ts-expect-error - version mismatch + foo: 'bar', + }); + + OpaqueMyType.createInstance('v1', { + // @ts-expect-error - version mismatch + bar: 'foo', + }); + + const myInstanceV1 = OpaqueMyType.createInstance('v1', { + foo: 'bar', + }); + + const myInstanceV2 = OpaqueMyType.createInstance(undefined, { + bar: 'foo', + }); + + expect(myInstanceV1.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstanceV1.version).toBe('v1'); + // @ts-expect-error - internal field not accessible + expect(myInstanceV1.foo).toBe('bar'); + + expect(myInstanceV2.$$type).toBe('my-type'); + // @ts-expect-error - version field not accessible + expect(myInstanceV2.version).toBe(undefined); + // @ts-expect-error - internal field not accessible + expect(myInstanceV2.bar).toBe('foo'); + + expect(OpaqueMyType.isType(myInstanceV1)).toBe(true); + expect(OpaqueMyType.isType(myInstanceV2)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); + + const myInternalV1 = OpaqueMyType.toInternal(myInstanceV1); + expect(myInternalV1).toBe(myInstanceV1); + // All fields accessible + expect(myInternalV1.$$type).toBe('my-type'); + expect(myInternalV1.version).toBe('v1'); + // @ts-expect-error - version has not been narrowed down + expect(myInternalV1.foo).toBe('bar'); + + const myInternalV2 = OpaqueMyType.toInternal(myInstanceV2); + expect(myInternalV2).toBe(myInstanceV2); + // All fields accessible + expect(myInternalV2.$$type).toBe('my-type'); + expect(myInternalV2.version).toBe(undefined); + // @ts-expect-error - version has not been narrowed down + expect(myInternalV2.bar).toBe('foo'); + + // Narrowing the version allows access to internal fields + expect(myInternalV1.version === 'v1' && myInternalV1.foo).toBe('bar'); + expect(myInternalV2.version === undefined && myInternalV2.bar).toBe('foo'); + + expect(OpaqueMyType.toInternal({ $$type: 'my-type' })).toBeDefined(); + expect(() => + OpaqueMyType.toInternal({ $$type: 'my-type', version: 'v3' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid opaque type instance, got version 'v3', expected undefined or 'v1'"`, + ); + }); + + it('should create an empty opaque type with no versions', () => { + type MyType = { + $$type: 'my-type'; + }; + + const OpaqueMyType = OpaqueType.create<{ + public: MyType; + versions: { + version: undefined; + }; + }>({ + type: 'my-type', + versions: [undefined], + }); + + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v0', { + foo: 'bar', + }); + + const myInstance = OpaqueMyType.createInstance(undefined, {}); + + expect(myInstance.$$type).toBe('my-type'); + + expect(OpaqueMyType.isType(myInstance)).toBe(true); + expect(OpaqueMyType.isType('hello')).toBe(false); + + const myInternal = OpaqueMyType.toInternal(myInstance); + expect(myInternal).toBe(myInstance); + // All fields accessible + expect(myInternal.$$type).toBe('my-type'); + expect(myInternal.version).toBe(undefined); + }); +}); diff --git a/packages/frontend-internal/src/wiring/OpaqueType.ts b/packages/frontend-internal/src/wiring/OpaqueType.ts new file mode 100644 index 0000000000..3d05b619bc --- /dev/null +++ b/packages/frontend-internal/src/wiring/OpaqueType.ts @@ -0,0 +1,168 @@ +/* + * Copyright 2024 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. + */ + +// TODO(Rugvip): This lives here temporarily, but should be moved to a more +// central location. It's useful for backend packages too so we'll need to have +// it in a common package, but it might also be that we want to make it +// available publicly too in which case it would make sense to have this be part +// of @backstage/version-bridge. The problem with exporting it from there is +// that it would need to be very stable at that point, so it might be a bit +// early to put it there already. + +/** + * A helper for working with opaque types. + */ +export class OpaqueType< + T extends { + public: { $$type: string }; + versions: { version: string | undefined }; + }, +> { + /** + * Creates a new opaque type. + * + * @param options.type The type identifier of the opaque type + * @param options.versions The available versions of the opaque type + * @returns A new opaque type helper + */ + static create< + T extends { + public: { $$type: string }; + versions: { version: string | undefined }; + }, + >(options: { + type: T['public']['$$type']; + versions: Array; + }) { + return new OpaqueType(options.type, new Set(options.versions)); + } + + #type: string; + #versions: Set; + + private constructor(type: string, versions: Set) { + this.#type = type; + this.#versions = versions; + } + + /** + * The internal version of the opaque type, used like this: `typeof MyOpaqueType.TPublic` + * + * @remarks + * + * This property is only useful for type checking, its runtime value is `undefined`. + */ + TPublic: T['public'] = undefined as any; + + /** + * The internal version of the opaque type, used like this: `typeof MyOpaqueType.TInternal` + * + * @remarks + * + * This property is only useful for type checking, its runtime value is `undefined`. + */ + TInternal: T['public'] & T['versions'] = undefined as any; + + /** + * @param value Input value expected to be an instance of this opaque type + * @returns True if the value matches this opaque type + */ + isType(value: unknown): value is T['public'] { + return this.#isThisInternalType(value); + } + + /** + * @param value Input value expected to be an instance of this opaque type + * @throws If the value is not an instance of this opaque type or is of an unsupported version + * @returns The internal version of the opaque type + */ + toInternal(value: unknown): T['public'] & T['versions'] { + if (!this.#isThisInternalType(value)) { + throw new TypeError( + `Invalid opaque type, expected '${ + this.#type + }', but got '${this.#stringifyUnknown(value)}'`, + ); + } + + if (!this.#versions.has(value.version)) { + const versions = Array.from(this.#versions).map(this.#stringifyVersion); + if (versions.length > 1) { + versions[versions.length - 1] = `or ${versions[versions.length - 1]}`; + } + const expected = + versions.length > 2 ? versions.join(', ') : versions.join(' '); + throw new TypeError( + `Invalid opaque type instance, got version ${this.#stringifyVersion( + value.version, + )}, expected ${expected}`, + ); + } + + return value; + } + + /** + * Creates an instance of the opaque type, returning the public type. + * + * @param version The version of the instance to create + * @param value The remaining public and internal properties of the instance + * @returns An instance of the opaque type + */ + createInstance< + TVersion extends T['versions']['version'], + TPublic extends T['public'], + >( + version: TVersion, + props: Omit & + (T['versions'] extends infer UVersion + ? UVersion extends { version: TVersion } + ? Omit + : never + : never) & + Object, // & Object to allow for object properties too, e.g. toString() + ): TPublic { + return { + ...(props as object), + $$type: this.#type, + ...(version && { version }), + } as unknown as TPublic; + } + + #isThisInternalType(value: unknown): value is T['public'] & T['versions'] { + if (value === null || typeof value !== 'object') { + return false; + } + return (value as T['public']).$$type === this.#type; + } + + #stringifyUnknown(value: unknown) { + if (typeof value !== 'object') { + return `<${typeof value}>`; + } + if (value === null) { + return ''; + } + if ('$$type' in value) { + return String(value.$$type); + } + return String(value); + } + + #stringifyVersion = (version: string | undefined) => { + return version ? `'${version}'` : 'undefined'; + }; +} diff --git a/packages/frontend-internal/src/wiring/index.ts b/packages/frontend-internal/src/wiring/index.ts index d0aefbed64..86f1ed9ab7 100644 --- a/packages/frontend-internal/src/wiring/index.ts +++ b/packages/frontend-internal/src/wiring/index.ts @@ -14,7 +14,4 @@ * limitations under the License. */ -export { - toInternalExtensionDefinition, - type InternalExtensionDefinition, -} from './InternalExtensionDefinition'; +export { OpaqueExtensionDefinition } from './InternalExtensionDefinition'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 7aef3fc1b5..ac7b1de432 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -31,7 +31,7 @@ import { import { ExtensionInput } from './createExtensionInput'; import { z } from 'zod'; import { createSchemaFromZod } from '../schema/createSchemaFromZod'; -import { InternalExtensionDefinition } from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; /** * Convert a single extension input into a matching resolved input. @@ -366,26 +366,6 @@ export function createExtension< namespace: string | undefined extends TNamespace ? undefined : TNamespace; name: string | undefined extends TName ? undefined : TName; }> { - type T = { - config: string extends keyof TConfigSchema - ? {} - : { - [key in keyof TConfigSchema]: z.infer>; - }; - configInput: string extends keyof TConfigSchema - ? {} - : z.input< - z.ZodObject<{ - [key in keyof TConfigSchema]: ReturnType; - }> - >; - output: UOutput; - inputs: TInputs; - kind: string | undefined extends TKind ? undefined : TKind; - namespace: string | undefined extends TNamespace ? undefined : TNamespace; - name: string | undefined extends TName ? undefined : TName; - }; - const schemaDeclaration = options.config?.schema; const configSchema = schemaDeclaration && @@ -397,10 +377,28 @@ export function createExtension< ), ); - return { - $$type: '@backstage/ExtensionDefinition', - version: 'v2', - T: undefined as unknown as T, + return OpaqueExtensionDefinition.createInstance('v2', { + T: undefined as unknown as { + config: string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + configInput: string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >; + output: UOutput; + inputs: TInputs; + kind: string | undefined extends TKind ? undefined : TKind; + namespace: string | undefined extends TNamespace ? undefined : TNamespace; + name: string | undefined extends TName ? undefined : TName; + }, kind: options.kind, namespace: options.namespace, name: options.name, @@ -512,5 +510,5 @@ export function createExtension< }, }) as ExtensionDefinition; }, - } as InternalExtensionDefinition; + }); } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 47ef7a75b4..16c763d725 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -29,12 +29,12 @@ import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; import { ExtensionDefinition } from './createExtension'; import { createExtensionDataContainer } from './createExtensionDataContainer'; -import { toInternalExtensionDefinition } from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; function unused(..._any: any[]) {} function factoryOutput(ext: ExtensionDefinition, inputs: unknown = undefined) { - const int = toInternalExtensionDefinition(ext); + const int = OpaqueExtensionDefinition.toInternal(ext); if (int.version !== 'v2') { throw new Error('Expected v2 extension'); } @@ -680,7 +680,7 @@ describe('createExtensionBlueprint', () => { }, }); - const ext = toInternalExtensionDefinition( + const ext = OpaqueExtensionDefinition.toInternal( blueprint.makeWithOverrides({ output: [testDataRef2], factory(origFactory) { diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendModule.ts b/packages/frontend-plugin-api/src/wiring/createFrontendModule.ts index e3fa8b6682..bece1539fa 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendModule.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendModule.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - InternalExtensionDefinition, - toInternalExtensionDefinition, -} from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; import { ExtensionDefinition } from './createExtension'; import { Extension, @@ -58,11 +55,11 @@ export function createFrontendModule< const extensions = new Array>(); const extensionDefinitionsById = new Map< string, - InternalExtensionDefinition + typeof OpaqueExtensionDefinition.TInternal >(); for (const def of options.extensions ?? []) { - const internal = toInternalExtensionDefinition(def); + const internal = OpaqueExtensionDefinition.toInternal(def); const ext = resolveExtensionDefinition(def, { namespace: pluginId }); extensions.push(ext); extensionDefinitionsById.set(ext.id, { diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts index adcd2b0867..865eae6444 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - InternalExtensionDefinition, - toInternalExtensionDefinition, -} from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; import { ExtensionDefinition } from './createExtension'; import { Extension, @@ -96,11 +93,11 @@ export function createFrontendPlugin< const extensions = new Array>(); const extensionDefinitionsById = new Map< string, - InternalExtensionDefinition + typeof OpaqueExtensionDefinition.TInternal >(); for (const def of options.extensions ?? []) { - const internal = toInternalExtensionDefinition(def); + const internal = OpaqueExtensionDefinition.toInternal(def); const ext = resolveExtensionDefinition(def, { namespace: options.id }); extensions.push(ext); extensionDefinitionsById.set(ext.id, { diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index a377cde2b9..53de20bff2 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -26,7 +26,7 @@ import { AnyExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; -import { toInternalExtensionDefinition } from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; /** @public */ export interface Extension { @@ -141,7 +141,7 @@ export function resolveExtensionDefinition< definition: ExtensionDefinition, context?: { namespace?: string }, ): Extension { - const internalDefinition = toInternalExtensionDefinition(definition); + const internalDefinition = OpaqueExtensionDefinition.toInternal(definition); const { name, kind, diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 5eda2d6e11..37ec1d8fd9 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -37,7 +37,7 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; import { TestApiRegistry } from '@backstage/test-utils'; -import { toInternalExtensionDefinition } from '@internal/frontend'; +import { OpaqueExtensionDefinition } from '@internal/frontend'; /** @public */ export class ExtensionQuery { @@ -105,7 +105,7 @@ export class ExtensionTester { ); } - const { name, namespace } = toInternalExtensionDefinition(extension); + const { name, namespace } = OpaqueExtensionDefinition.toInternal(extension); const definition = { ...extension, @@ -143,7 +143,7 @@ export class ExtensionTester { const tree = this.#resolveTree(); // Same fallback logic as in .add - const { name, namespace } = toInternalExtensionDefinition(extension); + const { name, namespace } = OpaqueExtensionDefinition.toInternal(extension); const definition = { ...extension, name: !namespace && !name ? 'test' : name,