Harmonize phantom .T getter behavior between ExtensionPoint and ServiceRef

Both `createExtensionPoint` and `createServiceRef` now consistently return
`null` from the phantom `.T` getter instead of throwing. Added `toJSON()` to
`ExtensionPoint` for parity with `ServiceRef`.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-13 19:48:42 +02:00
parent 925a63e27e
commit 213ebe77cc
5 changed files with 21 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': patch
---
Harmonized the phantom `.T` getter on `ExtensionPoint` to consistently return `null` instead of throwing.
@@ -25,6 +25,18 @@ const rootDep = createServiceRef<number>({ id: 'y', scope: 'root' });
const pluginDep = createServiceRef<boolean>({ id: 'z' });
function unused(..._any: any[]) {}
describe('createServiceRef', () => {
it('should create a ServiceRef', () => {
expect(ref.id).toBe('x');
expect(ref.scope).toBe('plugin');
expect(ref.T).toBe(null);
expect(String(ref)).toBe('serviceRef{x}');
expect(JSON.stringify(ref)).toBe(
'{"$$type":"@backstage/ServiceRef","id":"x","scope":"plugin","multiton":false}',
);
});
});
describe('createServiceFactory', () => {
it('should create a plugin scoped factory', () => {
const factory = createServiceFactory({
@@ -144,13 +144,12 @@ export function createServiceRef<
scope,
multiton,
get T(): TService {
throw new Error(`tried to read ServiceRef.T of ${this}`);
return null as TService;
},
toString() {
return `serviceRef{${options.id}}`;
},
toJSON() {
// This avoids accidental calls to T happening e.g. in tests
return {
$$type: '@backstage/ServiceRef',
id,
@@ -18,10 +18,10 @@ import { createExtensionPoint } from './createExtensionPoint';
describe('createExtensionPoint', () => {
it('should create an ExtensionPoint', () => {
const extensionPoint = createExtensionPoint({ id: 'x' });
const extensionPoint = createExtensionPoint<string>({ id: 'x' });
expect(extensionPoint).toBeDefined();
expect(extensionPoint.id).toBe('x');
expect(() => extensionPoint.T).not.toThrow();
expect(extensionPoint.T).toBe(null);
expect(String(extensionPoint)).toBe('extensionPoint{x}');
});
});
@@ -44,11 +44,7 @@ export function createExtensionPoint<T>(
return {
id: options.id,
get T(): T {
if (process.env.NODE_ENV === 'test') {
// Avoid throwing errors so tests asserting extensions' properties cannot be easily broken
return null as T;
}
throw new Error(`tried to read ExtensionPoint.T of ${this}`);
return null as T;
},
toString() {
return `extensionPoint{${options.id}}`;