make async and readonly

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2025-09-29 10:20:14 -04:00
committed by Fredrik Adelöw
parent 92f5823498
commit 6cf2ba39d9
6 changed files with 73 additions and 12 deletions
@@ -1093,7 +1093,9 @@ describe('BackendInitializer', () => {
instanceMetadata: coreServices.instanceMetadata,
},
async init({ instanceMetadata }) {
expect(instanceMetadata.getInstalledPlugins()).toEqual([
await expect(
instanceMetadata.getInstalledPlugins(),
).resolves.toEqual([
{
pluginId: 'test',
modules: [
@@ -1127,6 +1129,29 @@ describe('BackendInitializer', () => {
await backend.start();
});
it('should prevent writes to the instance metadata service', async () => {
expect.assertions(1);
const backend = new BackendInitializer(baseFactories);
const plugin = createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {
instanceMetadata: coreServices.instanceMetadata,
},
async init({ instanceMetadata }) {
const plugins = await instanceMetadata.getInstalledPlugins();
await expect(() => {
(plugins[0] as any).pluginId = 'foo';
}).toThrow(/Cannot assign to read only property/);
},
});
},
});
backend.add(plugin);
await backend.start();
});
it('should properly wait for all modules that consume an extension point to really finish, before starting the module that provides that extension point', async () => {
expect.assertions(3);
const backend = new BackendInitializer(baseFactories);
@@ -39,7 +39,7 @@ import { ForwardedError, ConflictError, assertError } from '@backstage/errors';
import { DependencyGraph } from '../lib/DependencyGraph';
import { ServiceRegistry } from './ServiceRegistry';
import { createInitializationLogger } from './createInitializationLogger';
import { unwrapFeature } from './helpers';
import { deepFreeze, unwrapFeature } from './helpers';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type { InstanceMetadataServicePluginInfo } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService';
@@ -135,13 +135,17 @@ function createInstanceMetadataServiceFactory(
logger: coreServices.rootLogger,
},
factory: async ({ logger }) => {
const readonlyInstalledPlugins = deepFreeze(
Object.values(installedPlugins),
);
const instanceMetadata = {
getInstalledPlugins: () => Object.values(installedPlugins),
getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins),
};
const plugins = await instanceMetadata.getInstalledPlugins();
logger.info(
`Installed plugins on this instance: ${instanceMetadata
.getInstalledPlugins()
`Installed plugins on this instance: ${plugins
.map(p => p.pluginId)
.join(', ')}`,
);
@@ -34,3 +34,29 @@ export function unwrapFeature(
return feature;
}
/** @internal */
export type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
/**
* Deeply freezes an object by recursively freezing all of its properties.
*
* - https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3
* - https://stackoverflow.com/a/69656011
*
* FIXME Should be part of Lodash and related: https://github.com/Maggi64/moderndash/issues/139
*
* Does not work with Set and Map: https://stackoverflow.com/q/31509175
*/
export function deepFreeze<
T,
// Can cause: "Type instantiation is excessively deep and possibly infinite."
>(obj: T) {
// @ts-expect-error
Object.values(obj).forEach(
value => Object.isFrozen(value) || deepFreeze(value),
);
return Object.freeze(obj) as DeepReadonly<T>;
}