diff --git a/packages/frontend-internal/src/wiring/OpaqueType.test.ts b/packages/frontend-internal/src/wiring/OpaqueType.test.ts index f952f9cdbb..2e49ca9029 100644 --- a/packages/frontend-internal/src/wiring/OpaqueType.test.ts +++ b/packages/frontend-internal/src/wiring/OpaqueType.test.ts @@ -33,42 +33,20 @@ describe('OpaqueType', () => { versions: ['v1'], }); - OpaqueMyType.createInstance({ - // @ts-expect-error - wrong type - $$type: 'wrong-type', - version: 'v1', - foo: 'bar', - }); - - OpaqueMyType.createInstance({ - $$type: 'my-type', - // @ts-expect-error - unsupported version - version: 'v2', - foo: 'bar', - }); - - // @ts-expect-error - missing version - OpaqueMyType.createInstance({ - $$type: 'my-type', + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v2', { foo: 'bar', }); // @ts-expect-error - missing internal field - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', - }); + OpaqueMyType.createInstance('v1', {}); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + OpaqueMyType.createInstance('v1', { // @ts-expect-error - invalid internal field foo: 3, }); - const myInstance = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + const myInstance = OpaqueMyType.createInstance('v1', { foo: 'bar', }); @@ -170,62 +148,34 @@ describe('OpaqueType', () => { versions: ['v1', 'v2', 'v3'], }); - OpaqueMyType.createInstance({ - // @ts-expect-error - wrong type - $$type: 'wrong-type', - version: 'v1', - foo: 'bar', - }); - - OpaqueMyType.createInstance({ - $$type: 'my-type', - // @ts-expect-error - unsupported version - version: 'v0', - foo: 'bar', - }); - - // @ts-expect-error - missing version - OpaqueMyType.createInstance({ - $$type: 'my-type', + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v0', { foo: 'bar', }); // @ts-expect-error - missing internal field - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', - }); + OpaqueMyType.createInstance('v1', {}); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + OpaqueMyType.createInstance('v1', { // @ts-expect-error - invalid internal field foo: 3, }); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v2', + OpaqueMyType.createInstance('v2', { // @ts-expect-error - version mismatch foo: 'bar', }); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + OpaqueMyType.createInstance('v1', { // @ts-expect-error - version mismatch bar: 'foo', }); - const myInstanceV1 = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + const myInstanceV1 = OpaqueMyType.createInstance('v1', { foo: 'bar', }); - const myInstanceV2 = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v2', + const myInstanceV2 = OpaqueMyType.createInstance('v2', { bar: 'foo', }); @@ -293,35 +243,20 @@ describe('OpaqueType', () => { versions: [undefined], }); - OpaqueMyType.createInstance({ - // @ts-expect-error - wrong type - $$type: 'wrong-type', - foo: 'bar', - }); - - OpaqueMyType.createInstance({ - $$type: 'my-type', - // @ts-expect-error - unsupported version - version: 'v1', + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v1', { foo: 'bar', }); // @ts-expect-error - missing internal field - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: undefined, - }); + OpaqueMyType.createInstance(undefined, {}); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: undefined, + OpaqueMyType.createInstance(undefined, { // @ts-expect-error - invalid internal field foo: 3, }); - const myInstance = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: undefined, + const myInstance = OpaqueMyType.createInstance(undefined, { foo: 'bar', }); @@ -370,62 +305,34 @@ describe('OpaqueType', () => { versions: [undefined, 'v1'], }); - OpaqueMyType.createInstance({ - // @ts-expect-error - wrong type - $$type: 'wrong-type', - version: 'v1', - foo: 'bar', - }); - - OpaqueMyType.createInstance({ - $$type: 'my-type', - // @ts-expect-error - unsupported version - version: 'v3', - foo: 'bar', - }); - - // @ts-expect-error - missing version - OpaqueMyType.createInstance({ - $$type: 'my-type', + // @ts-expect-error - unsupported version + OpaqueMyType.createInstance('v0', { foo: 'bar', }); // @ts-expect-error - missing internal field - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', - }); + OpaqueMyType.createInstance('v1', {}); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + OpaqueMyType.createInstance('v1', { // @ts-expect-error - invalid internal field foo: 3, }); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: undefined, + OpaqueMyType.createInstance(undefined, { // @ts-expect-error - version mismatch foo: 'bar', }); - OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + OpaqueMyType.createInstance('v1', { // @ts-expect-error - version mismatch bar: 'foo', }); - const myInstanceV1 = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: 'v1', + const myInstanceV1 = OpaqueMyType.createInstance('v1', { foo: 'bar', }); - const myInstanceV2 = OpaqueMyType.createInstance({ - $$type: 'my-type', - version: undefined, + const myInstanceV2 = OpaqueMyType.createInstance(undefined, { bar: 'foo', }); @@ -472,4 +379,38 @@ describe('OpaqueType', () => { `"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 index 5f571a5d4b..abd74bd910 100644 --- a/packages/frontend-internal/src/wiring/OpaqueType.ts +++ b/packages/frontend-internal/src/wiring/OpaqueType.ts @@ -116,14 +116,27 @@ export class OpaqueType< } /** - * Creates an instance of the opaque type, returning the public public type. + * Creates an instance of the opaque type, returning the public type. * - * By providing a type argument you can narrow the return to specific type parameters. + * @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( - value: T['public'] & T['versions'] & Object, // & Object to allow for object properties too, e.g. toString() - ): TBase { - return value as unknown as TBase; + createInstance( + 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() + ): T['public'] { + return { + ...(props as object), + $$type: this.#type, + ...(version && { version }), + } as T['public']; } #isThisInternalType(value: unknown): value is T['public'] & T['versions'] { diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 92c8bc7be9..4941324bed 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -366,6 +366,26 @@ 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 && @@ -377,30 +397,8 @@ export function createExtension< ), ); - return OpaqueExtensionDefinition.createInstance({ - $$type: '@backstage/ExtensionDefinition', - version: '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; - }, + return OpaqueExtensionDefinition.createInstance('v2', { + T: undefined as unknown as T, kind: options.kind, namespace: options.namespace, name: options.name, @@ -512,5 +510,5 @@ export function createExtension< }, }) as ExtensionDefinition; }, - }); + }) as ExtensionDefinition; }