diff --git a/.changeset/cuddly-clocks-dance.md b/.changeset/cuddly-clocks-dance.md new file mode 100644 index 0000000000..9d33535a62 --- /dev/null +++ b/.changeset/cuddly-clocks-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Added $select attribute to user query diff --git a/.changeset/eight-shrimps-call.md b/.changeset/eight-shrimps-call.md new file mode 100644 index 0000000000..2e19681f7a --- /dev/null +++ b/.changeset/eight-shrimps-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +humanizeEntityRef function can now be forced to include default namespace diff --git a/.changeset/empty-colts-whisper.md b/.changeset/empty-colts-whisper.md new file mode 100644 index 0000000000..1eabe8c58c --- /dev/null +++ b/.changeset/empty-colts-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +No longer log when reloading remote config. diff --git a/.changeset/flat-humans-dance.md b/.changeset/flat-humans-dance.md new file mode 100644 index 0000000000..ea7571f5bc --- /dev/null +++ b/.changeset/flat-humans-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Service are now scoped to either `'plugin'` or `'root'` scope. Service factories have been updated to provide dependency instances directly rather than factory functions. diff --git a/.changeset/lucky-ads-worry.md b/.changeset/lucky-ads-worry.md new file mode 100644 index 0000000000..a7f749da17 --- /dev/null +++ b/.changeset/lucky-ads-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Support displaying and ordering by counts in `EntityTagPicker` field. Add the `showCounts` option to enable this. Also support configuring `helperText`. diff --git a/.changeset/quick-items-invite.md b/.changeset/quick-items-invite.md new file mode 100644 index 0000000000..33f90cb662 --- /dev/null +++ b/.changeset/quick-items-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': patch +--- + +Updated usage of experimental backend service APIs. diff --git a/.changeset/slow-phones-count.md b/.changeset/slow-phones-count.md new file mode 100644 index 0000000000..ce2f9c8432 --- /dev/null +++ b/.changeset/slow-phones-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Updated service implementations and backend wiring to support scoped service. diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 0ff0bc230c..1408d732d3 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -49,6 +49,8 @@ The api for providing custom rules may differ between plugins, but there should // packages/backend/src/plugins/catalog.ts import { isInSystemRule } from './permission'; +// The CatalogBuilder with the addPermissionRules function is in the alpha path +import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha'; ... @@ -56,7 +58,7 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); - builder.addPermissionRules(isInSystem); + builder.addPermissionRules(isInSystemRule); ... return router; } diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 6daf448cdd..6e6bcd2d03 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -77,10 +77,7 @@ export default async function createPlugin( logger: env.logger, discovery: env.discovery, policy: new TestPermissionPolicy(), - identity: IdentityClient.create({ - discovery: env.discovery, - issuer: await env.discovery.getExternalBaseUrl('auth'), - }), + identity: env.identity, }); } ``` diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md new file mode 100644 index 0000000000..a26eb49a9b --- /dev/null +++ b/docs/plugins/feature-flags.md @@ -0,0 +1,64 @@ +--- +id: feature-flags +title: Feature Flags +description: Details the process of defining setting and reading a plugin feature flag. +--- + +Backstage offers the ability to define feature flags inside a plugin. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. + +This page describes the process of defining setting and reading a plugin feature flag. If you are looking for using feature flags with software templates that can be found under [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). + +## Defining a Feature Flag + +Before using a feature flag we must first define it. This is done when we create the plugin by passing the name of the feature flag into the `featureFlags` array. + +```ts +/* src/plugin.ts */ +import { createPlugin, createRouteRef } from '@backstage/core-plugin-api'; +import ExampleComponent from './components/ExampleComponent'; + +export const examplePlugin = createPlugin({ + id: 'example', + routes: { + root: rootRouteRef, + }, + featureFlags: [{ name: 'show-example-feature' }], +}); +``` + +## Enabling Feature Flags + +Feature flags are defaulted to off and can be updated by individual users in the backstage interface. + +These are set by navigating to the page under `Settings` > `Feature Flags`. + +The users selection is saved in the users browsers local storage. Once toggled it may be required for a user to refresh the page to see any new changes. + +## FeatureFlagged Component + +The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/docs/reference/core-app-api.featureflagged) component. + +```ts +import { FeatureFlagged } from '@backstage/core-app-api' + +... + + + + + + + + +``` + +## Evaluating Feature Flag State + +It is also possible to test the feature flag state using the [FeatureFlags Api](https://backstage.io/docs/reference/core-plugin-api.featureflagsapi). + +```ts +import { useApi, featureFlagsApiRef } from '@backstage/core-plugin-api'; + +const featureFlagsApi = useApi(featureFlagsApiRef); +const isOn = featureFlagsApi.isActive('show-example-feature'); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7368602298..0d386e63fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,10 +127,10 @@ nav: - Create a Backstage Plugin: 'plugins/create-a-plugin.md' - Plugin Development: 'plugins/plugin-development.md' - Structure of a plugin: 'plugins/structure-of-a-plugin.md' - - Plugin Development: 'plugins/plugin-development.md' - Integrate into the Software Catalog: 'plugins/integrating-plugin-into-software-catalog.md' - Composability System: 'plugins/composability.md' - Plugin Analytics: 'plugins/analytics.md' + - Feature Flags: 'plugins/feature-flags.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' diff --git a/packages/backend-app-api/src/services/implementations/cacheService.ts b/packages/backend-app-api/src/services/implementations/cacheService.ts index c5e58b4a41..7e15f031c6 100644 --- a/packages/backend-app-api/src/services/implementations/cacheService.ts +++ b/packages/backend-app-api/src/services/implementations/cacheService.ts @@ -18,6 +18,7 @@ import { CacheManager } from '@backstage/backend-common'; import { configServiceRef, createServiceFactory, + pluginMetadataServiceRef, cacheServiceRef, } from '@backstage/backend-plugin-api'; @@ -25,13 +26,13 @@ import { export const cacheFactory = createServiceFactory({ service: cacheServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const cacheManager = CacheManager.fromConfig(config); - return async (pluginId: string) => { - return cacheManager.forPlugin(pluginId); + return async ({ plugin }) => { + return cacheManager.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/configService.ts b/packages/backend-app-api/src/services/implementations/configService.ts index c4aa641f72..91327289df 100644 --- a/packages/backend-app-api/src/services/implementations/configService.ts +++ b/packages/backend-app-api/src/services/implementations/configService.ts @@ -19,23 +19,20 @@ import { configServiceRef, createServiceFactory, loggerToWinstonLogger, - loggerServiceRef, + rootLoggerServiceRef, } from '@backstage/backend-plugin-api'; /** @public */ export const configFactory = createServiceFactory({ service: configServiceRef, deps: { - loggerFactory: loggerServiceRef, + logger: rootLoggerServiceRef, }, - factory: async ({ loggerFactory }) => { - const logger = await loggerFactory('root'); + async factory({ logger }) { const config = await loadBackendConfig({ argv: process.argv, logger: loggerToWinstonLogger(logger), }); - return async () => { - return config; - }; + return config; }, }); diff --git a/packages/backend-app-api/src/services/implementations/databaseService.ts b/packages/backend-app-api/src/services/implementations/databaseService.ts index b2bc19de84..f6401528e6 100644 --- a/packages/backend-app-api/src/services/implementations/databaseService.ts +++ b/packages/backend-app-api/src/services/implementations/databaseService.ts @@ -19,19 +19,20 @@ import { configServiceRef, createServiceFactory, databaseServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; /** @public */ export const databaseFactory = createServiceFactory({ service: databaseServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const databaseManager = DatabaseManager.fromConfig(config); - return async (pluginId: string) => { - return databaseManager.forPlugin(pluginId); + return async ({ plugin }) => { + return databaseManager.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/discoveryService.ts b/packages/backend-app-api/src/services/implementations/discoveryService.ts index 3f1a584c61..7f35bf2447 100644 --- a/packages/backend-app-api/src/services/implementations/discoveryService.ts +++ b/packages/backend-app-api/src/services/implementations/discoveryService.ts @@ -25,10 +25,9 @@ import { export const discoveryFactory = createServiceFactory({ service: discoveryServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const discovery = SingleHostDiscovery.fromConfig(config); return async () => { return discovery; diff --git a/packages/backend-app-api/src/services/implementations/httpRouterService.ts b/packages/backend-app-api/src/services/implementations/httpRouterService.ts index 6460a77eaf..be4af664c4 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouterService.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouterService.ts @@ -18,6 +18,7 @@ import { createServiceFactory, httpRouterServiceRef, configServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; import { Handler } from 'express'; @@ -27,18 +28,20 @@ import { createServiceBuilder } from '@backstage/backend-common'; export const httpRouterFactory = createServiceFactory({ service: httpRouterServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { + async factory({ config }) { const rootRouter = Router(); const service = createServiceBuilder(module) - .loadConfig(await configFactory('root')) + .loadConfig(config) .addRouter('', rootRouter); await service.start(); - return async (pluginId?: string) => { + return async ({ plugin }) => { + const pluginId = plugin.getId(); const path = pluginId ? `/api/${pluginId}` : ''; return { use(handler: Handler) { diff --git a/packages/backend-app-api/src/services/implementations/loggerService.ts b/packages/backend-app-api/src/services/implementations/loggerService.ts index e90b591302..ff72020140 100644 --- a/packages/backend-app-api/src/services/implementations/loggerService.ts +++ b/packages/backend-app-api/src/services/implementations/loggerService.ts @@ -14,38 +14,23 @@ * limitations under the License. */ -import { createRootLogger } from '@backstage/backend-common'; import { createServiceFactory, - Logger, loggerServiceRef, + pluginMetadataServiceRef, + rootLoggerServiceRef, } from '@backstage/backend-plugin-api'; -import { Logger as WinstonLogger } from 'winston'; - -class BackstageLogger implements Logger { - static fromWinston(logger: WinstonLogger): BackstageLogger { - return new BackstageLogger(logger); - } - - private constructor(private readonly winston: WinstonLogger) {} - - info(message: string, ...meta: any[]): void { - this.winston.info(message, ...meta); - } - - child(fields: { [name: string]: string }): Logger { - return new BackstageLogger(this.winston.child(fields)); - } -} /** @public */ export const loggerFactory = createServiceFactory({ service: loggerServiceRef, - deps: {}, - factory: async () => { - const root = BackstageLogger.fromWinston(createRootLogger()); - return async (pluginId: string) => { - return root.child({ pluginId }); + deps: { + rootLogger: rootLoggerServiceRef, + plugin: pluginMetadataServiceRef, + }, + async factory({ rootLogger }) { + return async ({ plugin }) => { + return rootLogger.child({ pluginId: plugin.getId() }); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/permissionsService.ts b/packages/backend-app-api/src/services/implementations/permissionsService.ts index 26fe012a20..32e8a1a9f9 100644 --- a/packages/backend-app-api/src/services/implementations/permissionsService.ts +++ b/packages/backend-app-api/src/services/implementations/permissionsService.ts @@ -27,20 +27,16 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; export const permissionsFactory = createServiceFactory({ service: permissionsServiceRef, deps: { - configFactory: configServiceRef, - discoveryFactory: discoveryServiceRef, - tokenManagerFactory: tokenManagerServiceRef, + config: configServiceRef, + discovery: discoveryServiceRef, + tokenManager: tokenManagerServiceRef, }, - factory: async ({ configFactory, discoveryFactory, tokenManagerFactory }) => { - const config = await configFactory('root'); - const discovery = await discoveryFactory('root'); - const tokenManager = await tokenManagerFactory('root'); - const permissions = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - return async (_pluginId: string) => { - return permissions; + async factory({ config }) { + return async ({ discovery, tokenManager }) => { + return ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/rootLoggerService.ts b/packages/backend-app-api/src/services/implementations/rootLoggerService.ts new file mode 100644 index 0000000000..d7da11723e --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootLoggerService.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2022 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 { createRootLogger } from '@backstage/backend-common'; +import { + createServiceFactory, + Logger, + rootLoggerServiceRef, +} from '@backstage/backend-plugin-api'; +import { Logger as WinstonLogger } from 'winston'; + +class BackstageLogger implements Logger { + static fromWinston(logger: WinstonLogger): BackstageLogger { + return new BackstageLogger(logger); + } + + private constructor(private readonly winston: WinstonLogger) {} + + info(message: string, ...meta: any[]): void { + this.winston.info(message, ...meta); + } + + child(fields: { [name: string]: string }): Logger { + return new BackstageLogger(this.winston.child(fields)); + } +} + +/** @public */ +export const loggerFactory = createServiceFactory({ + service: rootLoggerServiceRef, + deps: {}, + async factory() { + return BackstageLogger.fromWinston(createRootLogger()); + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/schedulerService.ts b/packages/backend-app-api/src/services/implementations/schedulerService.ts index 39dbf26ba9..40676344ec 100644 --- a/packages/backend-app-api/src/services/implementations/schedulerService.ts +++ b/packages/backend-app-api/src/services/implementations/schedulerService.ts @@ -17,6 +17,7 @@ import { configServiceRef, createServiceFactory, + pluginMetadataServiceRef, schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { TaskScheduler } from '@backstage/backend-tasks'; @@ -25,13 +26,13 @@ import { TaskScheduler } from '@backstage/backend-tasks'; export const schedulerFactory = createServiceFactory({ service: schedulerServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const taskScheduler = TaskScheduler.fromConfig(config); - return async (pluginId: string) => { - return taskScheduler.forPlugin(pluginId); + return async ({ plugin }) => { + return taskScheduler.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/tokenManagerService.ts b/packages/backend-app-api/src/services/implementations/tokenManagerService.ts index 7767c17944..92f42c10db 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManagerService.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManagerService.ts @@ -27,32 +27,11 @@ import { ServerTokenManager } from '@backstage/backend-common'; export const tokenManagerFactory = createServiceFactory({ service: tokenManagerServiceRef, deps: { - configFactory: configServiceRef, - loggerFactory: loggerServiceRef, + config: configServiceRef, + logger: loggerServiceRef, }, - factory: async ({ configFactory, loggerFactory }) => { - const logger = await loggerFactory('root'); - const config = await configFactory('root'); - return async (_pluginId: string) => { - // doesn't the logger want to be inferred from the plugin tho here? - // maybe ... also why do we recreate it every time otherwise - // we should memoize on a per plugin right? so I think it's should be fine to re-use the plugin one - // we shouldn't recreate on a per plugin basis. - // hm - on the other hand, is this really ever called more than once? - // not this function right. should only be called when the plugin requests this serviceRef - // yeah so no need to worry about memo probably - // but we still want to scope the logger to the ServrTokenmanagfer>? - // mm sure maybe - // maybe in this case it doesn't provide so much value b - // oh hang on - isn't it up to THE MANAGER to make a child internally if it wants to do that - // so that it becomes a property intrinsic to that class, no matter how it's constructed - // or is that too much responsibility for it - making the constructor complex so to speak, making it harder to tweak that behavior - // this is not ultra efficient :) - - // I think the naming here is wrong to be gonest - // this isn't like the cache manager or the database manager - // the manager name is confusuion i think - // aye perhaps + async factory() { + return async ({ config, logger }) => { return ServerTokenManager.fromConfig(config, { logger: loggerToWinstonLogger(logger), }); diff --git a/packages/backend-app-api/src/services/implementations/urlReaderService.ts b/packages/backend-app-api/src/services/implementations/urlReaderService.ts index df353a52d2..d7d7502a08 100644 --- a/packages/backend-app-api/src/services/implementations/urlReaderService.ts +++ b/packages/backend-app-api/src/services/implementations/urlReaderService.ts @@ -27,15 +27,14 @@ import { export const urlReaderFactory = createServiceFactory({ service: urlReaderServiceRef, deps: { - configFactory: configServiceRef, - loggerFactory: loggerServiceRef, + config: configServiceRef, + logger: loggerServiceRef, }, - factory: async ({ configFactory, loggerFactory }) => { - return async (pluginId: string) => { - const logger = await loggerFactory(pluginId); + async factory() { + return async ({ config, logger }) => { return UrlReaders.default({ + config, logger: loggerToWinstonLogger(logger), - config: await configFactory(pluginId), }); }; }, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index afb5250504..c86cd8382d 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -50,11 +50,12 @@ export class BackendInitializer { if (extensionPoint) { result.set(name, extensionPoint); } else { - const factory = await this.#serviceHolder.get( + const impl = await this.#serviceHolder.get( ref as ServiceRef, + pluginId, ); - if (factory) { - result.set(name, await factory(pluginId)); + if (impl) { + result.set(name, impl); } else { missingRefs.add(ref); } diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index a00f6c0b4b..897ed4e3cd 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -18,158 +18,209 @@ import { createServiceRef, createServiceFactory, ServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import { ServiceRegistry } from './ServiceRegistry'; -const ref1 = createServiceRef<{ x: number; pluginId: string }>({ +const ref1 = createServiceRef<{ x: number }>({ id: '1', }); const sf1 = createServiceFactory({ service: ref1, deps: {}, - factory: async () => { - return async pluginId => { - return { x: 1, pluginId }; + async factory() { + return async () => { + return { x: 1 }; }; }, }); -const ref2 = createServiceRef<{ x: number; pluginId: string }>({ +const ref2 = createServiceRef<{ x: number }>({ + scope: 'root', id: '2', }); const sf2 = createServiceFactory({ service: ref2, deps: {}, - factory: async () => { - return async pluginId => { - return { x: 2, pluginId }; - }; + async factory() { + return { x: 2 }; }, }); const sf2b = createServiceFactory({ service: ref2, deps: {}, - factory: async () => { - return async pluginId => { - return { x: 22, pluginId }; - }; + async factory() { + return { x: 22 }; }, -}); +})(); -const refDefault1 = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault1 = createServiceRef<{ x: number }>({ id: '1', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 10, pluginId }), - }), + async factory() { + return async () => ({ x: 10 }); + }, + })(), }); -const refDefault2a = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault2a = createServiceRef<{ x: number }>({ id: '2a', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 20, pluginId }), + async factory() { + return async () => ({ x: 20 }); + }, }), }); -const refDefault2b = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault2b = createServiceRef<{ x: number }>({ id: '2b', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 220, pluginId }), + async factory() { + return async () => ({ x: 220 }); + }, }), }); describe('ServiceRegistry', () => { it('should return undefined if there is no factory defined', async () => { const registry = new ServiceRegistry([]); - expect(registry.get(ref1)).toBe(undefined); + expect(registry.get(ref1, 'catalog')).toBe(undefined); }); - it('should return a factory for a registered ref', async () => { + it('should return an implementation for a registered ref', async () => { const registry = new ServiceRegistry([sf1]); - const factory = registry.get(ref1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ - x: 1, - pluginId: 'catalog', - }); - await expect(factory('scaffolder')).resolves.toEqual({ - x: 1, - pluginId: 'scaffolder', - }); - expect(await factory('catalog')).toBe(await factory('catalog')); + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 }); + await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 }); + expect(await registry.get(ref1, 'catalog')).toBe( + await registry.get(ref1, 'catalog'), + ); + expect(await registry.get(ref1, 'scaffolder')).toBe( + await registry.get(ref1, 'scaffolder'), + ); + expect(await registry.get(ref1, 'catalog')).not.toBe( + await registry.get(ref1, 'scaffolder'), + ); }); it('should handle multiple factories with different serviceRefs', async () => { const registry = new ServiceRegistry([sf1, sf2]); - const factory1 = registry.get(ref1)!; - const factory2 = registry.get(ref2)!; - expect(factory1).toEqual(expect.any(Function)); - expect(factory2).toEqual(expect.any(Function)); - await expect(factory1('catalog')).resolves.toEqual({ + + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1, - pluginId: 'catalog', }); - await expect(factory2('catalog')).resolves.toEqual({ + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ x: 2, + }); + expect(await registry.get(ref1, 'catalog')).not.toBe( + await registry.get(ref2, 'catalog'), + ); + }); + + it('should not be possible for root scoped services to depend on plugin scoped services', async () => { + const factory = createServiceFactory({ + service: ref2, + deps: { pluginDep: ref1 }, + async factory() { + return { x: 2 }; + }, + }); + const registry = new ServiceRegistry([factory, sf1]); + await expect(registry.get(ref2, 'catalog')).rejects.toThrow( + "Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.", + ); + }); + + it('should be possible for plugin scoped services to depend on root scoped services', async () => { + const factory = createServiceFactory({ + service: ref1, + deps: { rootDep: ref2 }, + async factory({ rootDep }) { + return async () => ({ x: rootDep.x }); + }, + }); + const registry = new ServiceRegistry([factory, sf2]); + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ + x: 2, + }); + }); + + it('should be possible for root scoped services to depend on root scoped services', async () => { + const ref = createServiceRef<{ x: number }>({ id: 'x', scope: 'root' }); + const factory = createServiceFactory({ + service: ref, + deps: { rootDep: ref2 }, + async factory({ rootDep }) { + return { x: rootDep.x }; + }, + }); + const registry = new ServiceRegistry([factory, sf2]); + await expect(registry.get(ref, 'catalog')).resolves.toEqual({ + x: 2, + }); + }); + + it('should return the pluginId from the pluginMetadata service', async () => { + const ref = createServiceRef<{ pluginId: string }>({ id: 'x' }); + const factory = createServiceFactory({ + service: ref, + deps: { meta: pluginMetadataServiceRef }, + async factory() { + return async ({ meta }) => ({ pluginId: meta.getId() }); + }, + }); + const registry = new ServiceRegistry([factory]); + await expect(registry.get(ref, 'catalog')).resolves.toEqual({ pluginId: 'catalog', }); - expect(await factory1('catalog')).not.toBe(await factory2('catalog')); }); it('should use the last factory for each ref', async () => { const registry = new ServiceRegistry([sf2, sf2b]); - const factory2 = registry.get(ref2)!; - await expect(factory2('catalog')).resolves.toEqual({ + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ x: 22, - pluginId: 'catalog', }); }); - it('should return the defaultFactory from the ref if not provided to the registry', async () => { + it('should use the defaultFactory from the ref if not provided to the registry', async () => { const registry = new ServiceRegistry([]); - const factory = registry.get(refDefault1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 10, - pluginId: 'catalog', }); }); - it('should not return the defaultFactory from the ref if provided to the registry', async () => { + it('should not use the defaultFactory from the ref if provided to the registry', async () => { const registry = new ServiceRegistry([sf1]); - const factory = registry.get(refDefault1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 1, - pluginId: 'catalog', }); }); it('should handle duplicate defaultFactories by duplicating the implementations', async () => { const registry = new ServiceRegistry([]); - const factoryA = registry.get(refDefault2a)!; - const factoryB = registry.get(refDefault2b)!; - expect(factoryA).toEqual(expect.any(Function)); - expect(factoryB).toEqual(expect.any(Function)); - await expect(factoryA('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({ x: 20, - pluginId: 'catalog', }); - await expect(factoryB('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault2b, 'catalog')).resolves.toEqual({ x: 220, - pluginId: 'catalog', }); - expect(await factoryA('catalog')).toBe(await factoryA('catalog')); - expect(await factoryB('catalog')).toBe(await factoryB('catalog')); - expect(await factoryA('catalog')).not.toBe(await factoryB('catalog')); + expect(await registry.get(refDefault2a, 'catalog')).toBe( + await registry.get(refDefault2a, 'catalog'), + ); + expect(await registry.get(refDefault2b, 'catalog')).toBe( + await registry.get(refDefault2b, 'catalog'), + ); + expect(await registry.get(refDefault2a, 'catalog')).not.toBe( + await registry.get(refDefault2b, 'catalog'), + ); }); it('should only call each default factory loader once', async () => { @@ -177,7 +228,9 @@ describe('ServiceRegistry', () => { createServiceFactory({ service, deps: {}, - factory: async () => async () => {}, + async factory() { + return async () => {}; + }, }), ); const ref = createServiceRef({ @@ -186,17 +239,16 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([]); - const factory = registry.get(ref)!; await Promise.all([ - expect(factory('catalog')).resolves.toBeUndefined(), - expect(factory('catalog')).resolves.toBeUndefined(), + expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), + expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), ]); expect(factoryLoader).toHaveBeenCalledTimes(1); }); it('should not call factory functions more than once', async () => { - const innerFactory = jest.fn(async (pluginId: string) => { - return { x: 1, pluginId }; + const innerFactory = jest.fn(async () => { + return { x: 1 }; }); const factory = jest.fn(async () => innerFactory); const myFactory = createServiceFactory({ @@ -208,17 +260,15 @@ describe('ServiceRegistry', () => { const registry = new ServiceRegistry([myFactory]); await Promise.all([ - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('scaffolder')!, - registry.get(ref1)!('scaffolder')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'scaffolder')!, + registry.get(ref1, 'scaffolder')!, ]); expect(factory).toHaveBeenCalledTimes(1); expect(innerFactory).toHaveBeenCalledTimes(2); - expect(innerFactory).toHaveBeenCalledWith('catalog'); - expect(innerFactory).toHaveBeenCalledWith('scaffolder'); }); it('should throw if dependencies are not available', async () => { @@ -231,9 +281,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'", ); }); @@ -247,8 +296,8 @@ describe('ServiceRegistry', () => { const factoryA = createServiceFactory({ service: refA, deps: { b: refB }, - async factory({ b }) { - return async pluginId => b(pluginId); + async factory() { + return async ({ b }) => b; }, }); @@ -261,9 +310,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([factoryA, factoryB]); - const factory = registry.get(refA)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(refA, 'catalog')).rejects.toThrow( "Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'", ); }); @@ -278,9 +326,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error", ); }); @@ -290,17 +337,16 @@ describe('ServiceRegistry', () => { service: ref1, deps: {}, async factory() { - return pluginId => { - throw new Error(`error in plugin ${pluginId}`); + return () => { + throw new Error(`error in plugin`); }; }, }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( - "Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin catalog", + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( + "Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin", ); }); @@ -313,9 +359,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([]); - const factory = registry.get(ref)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error", ); }); diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index e0509aecde..007ca00c88 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -16,8 +16,8 @@ import { ServiceFactory, - FactoryFunc, ServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; @@ -37,7 +37,9 @@ export class ServiceRegistry { readonly #implementations: Map< ServiceFactory, { - factoryFunc: Promise>; + factoryFunc: Promise< + (deps: { [name in string]: unknown }) => Promise + >; byPlugin: Map>; } >; @@ -58,67 +60,123 @@ export class ServiceRegistry { this.#implementations = new Map(); } - get(ref: ServiceRef): FactoryFunc | undefined { - let factory = this.#providedFactories.get(ref.id); - const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; - if (!factory && !defaultFactory) { + #resolveFactory( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { + // Special case handling of the plugin metadata service, generating a custom factory for it each time + if (ref.id === pluginMetadataServiceRef.id) { + return Promise.resolve({ + scope: 'plugin', + service: pluginMetadataServiceRef, + deps: {}, + factory: async () => async () => ({ + getId() { + return pluginId; + }, + }), + }); + } + + let resolvedFactory: Promise | ServiceFactory | undefined = + this.#providedFactories.get(ref.id); + const { __defaultFactory: defaultFactory } = + ref as InternalServiceRef; + if (!resolvedFactory && !defaultFactory) { return undefined; } - return async (pluginId: string): Promise => { - if (!factory) { - let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!); - if (!loadedFactory) { - loadedFactory = Promise.resolve() - .then(() => defaultFactory!(ref)) - .then(f => - typeof f === 'function' ? f() : f, - ) as Promise; - this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); - } - // NOTE: This await is safe as long as #providedFactories is not mutated. - factory = await loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, + if (!resolvedFactory) { + let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!); + if (!loadedFactory) { + loadedFactory = Promise.resolve() + .then(() => defaultFactory!(ref)) + .then(f => + typeof f === 'function' ? f() : f, + ) as Promise; + this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); + } + resolvedFactory = loadedFactory.catch(error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }); + } + + return Promise.resolve(resolvedFactory); + } + + #separateMapForTheRootService = new Map>(); + + #checkForMissingDeps(factory: ServiceFactory, pluginId: string) { + const missingDeps = Object.values(factory.deps).filter(ref => { + if (ref.id === pluginMetadataServiceRef.id) { + return false; + } + if (this.#providedFactories.get(ref.id)) { + return false; + } + + return !(ref as InternalServiceRef).__defaultFactory; + }); + + if (missingDeps.length) { + const missing = missingDeps.map(r => `'${r.id}'`).join(', '); + throw new Error( + `Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`, + ); + } + } + + get(ref: ServiceRef, pluginId: string): Promise | undefined { + return this.#resolveFactory(ref, pluginId)?.then(factory => { + if (factory.scope === 'root') { + let existing = this.#separateMapForTheRootService.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array>(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries)), ); - }); + this.#separateMapForTheRootService.set(factory, existing); + } + return existing as Promise; } let implementation = this.#implementations.get(factory); if (!implementation) { - const missingRefs = new Array>(); - const factoryDeps: { [name in string]: FactoryFunc } = {}; + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array>(); for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef); - if (!target) { - missingRefs.push(serviceRef); - } else { - factoryDeps[name] = target; + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); } } - if (missingRefs.length) { - const missing = missingRefs.map(r => `'${r.id}'`).join(', '); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`, - ); - } - implementation = { - factoryFunc: Promise.resolve() - .then(() => factory!.factory(factoryDeps)) + factoryFunc: Promise.all(rootDeps) + .then(entries => factory.factory(Object.fromEntries(entries))) .catch(error => { + const cause = stringifyError(error); throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the top-level factory function threw an error, ${stringifyError( - error, - )}`, + `Failed to instantiate service '${ref.id}' because the top-level factory function threw an error, ${cause}`, ); }), byPlugin: new Map(), @@ -129,24 +187,29 @@ export class ServiceRegistry { let result = implementation.byPlugin.get(pluginId) as Promise; if (!result) { - result = implementation.factoryFunc.then(func => - Promise.resolve() - .then(() => func(pluginId)) - .catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' for '${pluginId}' because the factory function threw an error, ${stringifyError( - error, - )}`, - ); - }), - ); + const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } + + result = implementation.factoryFunc + .then(func => + Promise.all(allDeps).then(entries => + func(Object.fromEntries(entries)), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); implementation.byPlugin.set(pluginId, result); } return result; - }; + }); } } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index febc6830c7..40ce6aa1ae 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -18,7 +18,6 @@ import { ServiceFactory, BackendFeature, ExtensionPoint, - FactoryFunc, ServiceRef, } from '@backstage/backend-plugin-api'; import { BackstageBackend } from './BackstageBackend'; @@ -47,7 +46,7 @@ export interface CreateSpecializedBackendOptions { } export type ServiceHolder = { - get(api: ServiceRef): FactoryFunc | undefined; + get(api: ServiceRef, pluginId: string): Promise | undefined; }; /** diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 30e58237af..3344576db8 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -66,10 +66,10 @@ export interface BackendRegistrationPoints { } // @public (undocumented) -export const cacheServiceRef: ServiceRef; +export const cacheServiceRef: ServiceRef; // @public (undocumented) -export const configServiceRef: ServiceRef; +export const configServiceRef: ServiceRef; // @public (undocumented) export function createBackendModule< @@ -105,22 +105,25 @@ export function createExtensionPoint(options: { // @public (undocumented) export function createServiceFactory< TService, + TScope extends 'root' | 'plugin', TImpl extends TService, TDeps extends { - [name in string]: unknown; + [name in string]: ServiceRef; }, TOpts extends | { [name in string]: unknown; } | undefined = undefined, ->(factory: { - service: ServiceRef; - deps: TypesToServiceRef; +>(config: { + service: ServiceRef; + deps: TDeps; factory( - deps: DepsToDepFactories, + deps: ServiceRefsToInstances, options: TOpts, - ): Promise>; + ): TScope extends 'root' + ? Promise + : Promise<(deps: ServiceRefsToInstances) => Promise>; }): undefined extends TOpts ? (options?: TOpts) => ServiceFactory : (options: TOpts) => ServiceFactory; @@ -128,21 +131,26 @@ export function createServiceFactory< // @public (undocumented) export function createServiceRef(options: { id: string; + scope?: 'plugin'; defaultFactory?: ( - service: ServiceRef, + service: ServiceRef, ) => Promise | (() => ServiceFactory)>; -}): ServiceRef; +}): ServiceRef; // @public (undocumented) -export const databaseServiceRef: ServiceRef; +export function createServiceRef(options: { + id: string; + scope: 'root'; + defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; +}): ServiceRef; // @public (undocumented) -export type DepsToDepFactories = { - [key in keyof T]: (pluginId: string) => Promise; -}; +export const databaseServiceRef: ServiceRef; // @public (undocumented) -export const discoveryServiceRef: ServiceRef; +export const discoveryServiceRef: ServiceRef; // @public export type ExtensionPoint = { @@ -152,9 +160,6 @@ export type ExtensionPoint = { $$ref: 'extension-point'; }; -// @public (undocumented) -export type FactoryFunc = (pluginId: string) => Promise; - // @public (undocumented) export interface HttpRouterService { // (undocumented) @@ -162,7 +167,7 @@ export interface HttpRouterService { } // @public (undocumented) -export const httpRouterServiceRef: ServiceRef; +export const httpRouterServiceRef: ServiceRef; // @public (undocumented) export interface Logger { @@ -173,7 +178,7 @@ export interface Logger { } // @public (undocumented) -export const loggerServiceRef: ServiceRef; +export const loggerServiceRef: ServiceRef; // @public (undocumented) export function loggerToWinstonLogger( @@ -183,33 +188,66 @@ export function loggerToWinstonLogger( // @public (undocumented) export const permissionsServiceRef: ServiceRef< - PermissionAuthorizer | PermissionEvaluator + PermissionAuthorizer | PermissionEvaluator, + 'plugin' >; // @public (undocumented) -export const schedulerServiceRef: ServiceRef; +export interface PluginMetadata { + // (undocumented) + getId(): string; +} // @public (undocumented) -export type ServiceFactory = { - service: ServiceRef; - deps: { - [key in string]: ServiceRef; - }; - factory(deps: { - [key in string]: unknown; - }): Promise>; -}; +export const pluginMetadataServiceRef: ServiceRef; + +// @public (undocumented) +export const rootLoggerServiceRef: ServiceRef; + +// @public (undocumented) +export const schedulerServiceRef: ServiceRef; + +// @public (undocumented) +export type ServiceFactory = + | { + scope: 'root'; + service: ServiceRef; + deps: { + [key in string]: ServiceRef; + }; + factory(deps: { + [key in string]: unknown; + }): Promise; + } + | { + scope: 'plugin'; + service: ServiceRef; + deps: { + [key in string]: ServiceRef; + }; + factory(deps: { + [key in string]: unknown; + }): Promise< + (deps: { + [key in string]: unknown; + }) => Promise + >; + }; // @public -export type ServiceRef = { +export type ServiceRef< + TService, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', +> = { id: string; - T: T; + scope: TScope; + T: TService; toString(): string; $$ref: 'service'; }; // @public (undocumented) -export const tokenManagerServiceRef: ServiceRef; +export const tokenManagerServiceRef: ServiceRef; // @public (undocumented) export type TypesToServiceRef = { @@ -217,5 +255,5 @@ export type TypesToServiceRef = { }; // @public (undocumented) -export const urlReaderServiceRef: ServiceRef; +export const urlReaderServiceRef: ServiceRef; ``` diff --git a/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts index ba5dcacdc8..f17c5f57bc 100644 --- a/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts @@ -21,5 +21,6 @@ import { createServiceRef } from '../system/types'; * @public */ export const configServiceRef = createServiceRef({ - id: 'core.config', + id: 'core.root.config', + scope: 'root', }); diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 797cfb5a76..e5f032ef60 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -26,3 +26,6 @@ export { discoveryServiceRef } from './discoveryServiceRef'; export { tokenManagerServiceRef } from './tokenManagerServiceRef'; export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; +export { rootLoggerServiceRef } from './rootLoggerServiceRef'; +export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; +export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts new file mode 100644 index 0000000000..3af6e54900 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 { createServiceRef } from '../system/types'; + +/** + * @public + */ +export interface PluginMetadata { + getId(): string; +} + +/** + * @public + */ +export const pluginMetadataServiceRef = createServiceRef({ + id: 'core.plugin-metadata', +}); diff --git a/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts new file mode 100644 index 0000000000..62e22c53d9 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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 { createServiceRef } from '../system/types'; +import { Logger } from './loggerServiceRef'; + +/** + * @public + */ +export const rootLoggerServiceRef = createServiceRef({ + id: 'core.root.logger', + scope: 'root', +}); diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 817b0a590f..8c666af42e 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -14,11 +14,5 @@ * limitations under the License. */ -export type { - ServiceRef, - TypesToServiceRef, - DepsToDepFactories, - FactoryFunc, - ServiceFactory, -} from './types'; +export type { ServiceRef, TypesToServiceRef, ServiceFactory } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index a19c17bb9c..d20fd6fd8f 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -19,63 +19,87 @@ * * @public */ -export type ServiceRef = { +export type ServiceRef< + TService, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', +> = { id: string; + /** + * This determines the scope at which this service is available. + * + * Root scoped services are available to all other services but + * may only depend on other root scoped services. + * + * Plugin scoped services are only available to other plugin scoped + * services but may depend on all other services. + */ + scope: TScope; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. */ - T: T; + T: TService; toString(): string; $$ref: 'service'; }; -/** - * @internal - */ -export type InternalServiceRef = ServiceRef & { - /** - * The default factory that will be used to create service - * instances if no other factory is provided. - */ - __defaultFactory?: ( - service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; -}; - /** @public */ export type TypesToServiceRef = { [key in keyof T]: ServiceRef }; /** @public */ -export type DepsToDepFactories = { - [key in keyof T]: (pluginId: string) => Promise; -}; +export type ServiceFactory = + | { + // This scope prop is needed in addition to the service ref, as TypeScript + // can't properly discriminate the two factory types otherwise. + scope: 'root'; + service: ServiceRef; + deps: { [key in string]: ServiceRef }; + factory(deps: { [key in string]: unknown }): Promise; + } + | { + scope: 'plugin'; + service: ServiceRef; + deps: { [key in string]: ServiceRef }; + factory(deps: { [key in string]: unknown }): Promise< + (deps: { [key in string]: unknown }) => Promise + >; + }; /** @public */ -export type FactoryFunc = (pluginId: string) => Promise; - -/** @public */ -export type ServiceFactory = { - service: ServiceRef; - deps: { [key in string]: ServiceRef }; - factory(deps: { [key in string]: unknown }): Promise>; -}; - -/** - * @public - */ export function createServiceRef(options: { id: string; + scope?: 'plugin'; defaultFactory?: ( - service: ServiceRef, + service: ServiceRef, ) => Promise | (() => ServiceFactory)>; +}): ServiceRef; +/** @public */ +export function createServiceRef(options: { + id: string; + scope: 'root'; + defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; +}): ServiceRef; +export function createServiceRef(options: { + id: string; + scope?: 'plugin' | 'root'; + defaultFactory?: + | (( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>) + | (( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>); }): ServiceRef { - const { id, defaultFactory } = options; + const { id, scope = 'plugin', defaultFactory } = options; return { id, + scope, get T(): T { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -84,32 +108,51 @@ export function createServiceRef(options: { }, $$ref: 'service', // TODO: declare __defaultFactory: defaultFactory, - } as InternalServiceRef; + } as ServiceRef & { + __defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; + }; } +/** @ignore */ +type ServiceRefsToInstances< + T extends { [key in string]: ServiceRef }, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', +> = { + [name in { + [key in keyof T]: T[key] extends ServiceRef ? key : never; + }[keyof T]]: T[name] extends ServiceRef ? TImpl : never; +}; + /** * @public */ export function createServiceFactory< TService, + TScope extends 'root' | 'plugin', TImpl extends TService, - TDeps extends { [name in string]: unknown }, + TDeps extends { [name in string]: ServiceRef }, TOpts extends { [name in string]: unknown } | undefined = undefined, ->(factory: { - service: ServiceRef; - deps: TypesToServiceRef; +>(config: { + service: ServiceRef; + deps: TDeps; factory( - deps: DepsToDepFactories, + deps: ServiceRefsToInstances, options: TOpts, - ): Promise>; + ): TScope extends 'root' + ? Promise + : Promise<(deps: ServiceRefsToInstances) => Promise>; }): undefined extends TOpts ? (options?: TOpts) => ServiceFactory : (options: TOpts) => ServiceFactory { - return (options?: TOpts) => ({ - service: factory.service, - deps: factory.deps, - factory(deps: DepsToDepFactories) { - return factory.factory(deps, options!); - }, - }); + return (options?: TOpts) => + ({ + scope: config.service.scope, + service: config.service, + deps: config.deps, + factory(deps: ServiceRefsToInstances) { + return config.factory(deps, options!); + }, + } as ServiceFactory); } diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 9537d2d945..f5ce0eb80c 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -287,13 +287,10 @@ export async function loadConfig( let handle: NodeJS.Timeout | undefined; try { handle = setInterval(async () => { - console.info(`Checking for config update`); const newRemoteConfigs = await loadRemoteConfigFiles(); if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) { remoteConfigs = newRemoteConfigs; - console.info(`Remote config change, reloading config ...`); watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]); - console.info(`Remote config reloaded`); } }, remoteProp.reloadIntervalSeconds * 1000); } catch (error) { @@ -303,7 +300,6 @@ export async function loadConfig( if (watchProp.stopSignal) { watchProp.stopSignal.then(() => { if (handle !== undefined) { - console.info(`Stopping remote config watch`); clearInterval(handle); handle = undefined; } diff --git a/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx index 1ff80fcacc..ac54780f7c 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx @@ -251,7 +251,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { await new Promise(r => setTimeout(r, 500)); - rendered.debug(); await expect( rendered.findByText('Path at inside: /foo/bar'), ).resolves.toBeInTheDocument(); @@ -344,7 +343,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { await expect( rendered.findByText('Path at inside: /foo/blob/baz'), ).resolves.toBeInTheDocument(); - rendered.debug(); }); it('should throw errors for routing to other routeRefs with unsupported parameters', () => { @@ -352,6 +350,7 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { const root = ( + } /> }> { await expect( rendered.findByText('Path at inside: /foo/blob/baz'), ).resolves.toBeInTheDocument(); - rendered.debug(); }); it('should throw errors for routing to other routeRefs with unsupported parameters', () => { const root = ( + } /> }> ( - {props.edge.label} +
{props.edge.label}
)); const minProps = { @@ -53,8 +53,7 @@ const edgeWithLabel = { ...edge, label }; describe('', () => { beforeEach(() => { - // jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement - Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { value: () => ({ width: 100, height: 100 }), configurable: true, }); @@ -63,26 +62,40 @@ describe('', () => { afterEach(jest.clearAllMocks); it('does not render the supplied label element if label is missing', () => { - const { container } = render(); + const { container } = render( + + + , + ); expect(container.getElementsByTagName('g')).toHaveLength(0); }); it('renders the supplied label element if label is present', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(label)).toBeInTheDocument(); }); it('passes down edge properties to the render method if label is present', () => { const edgeWithRandomProp = { ...edge, label, randomProp: true }; render( - , + + + , ); expect(renderElement).toHaveBeenCalledWith({ edge: edgeWithRandomProp }); }); it('calls setEdge with edge ID and actual label size after rendering', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(label)).toBeInTheDocument(); // Updates the edge in the graph diff --git a/packages/core-components/src/components/DependencyGraph/Node.test.tsx b/packages/core-components/src/components/DependencyGraph/Node.test.tsx index aaba09c004..ea69220336 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.test.tsx @@ -23,7 +23,7 @@ import { RenderNodeProps } from './types'; const node = { id: 'abc', x: 0, y: 0, width: 0, height: 0 }; const setNode = jest.fn(() => new dagre.graphlib.Graph()); const renderElement = jest.fn((props: RenderNodeProps) => ( - {props.node.id} +
{props.node.id}
)); const minProps = { @@ -34,8 +34,7 @@ const minProps = { describe('', () => { beforeEach(() => { - // jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement - Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { value: () => ({ width: 100, height: 100 }), configurable: true, }); @@ -44,19 +43,31 @@ describe('', () => { afterEach(jest.clearAllMocks); it('renders the supplied element', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(minProps.node.id)).toBeInTheDocument(); }); it('passes down node properties to the render method', () => { const nodeWithRandomProp = { ...node, randomProp: true }; - render(); + render( + + + , + ); expect(renderElement).toHaveBeenCalledWith({ node: nodeWithRandomProp }); }); it('calls setNode with node ID and actual size after rendering', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(minProps.node.id)).toBeInTheDocument(); // Updates the node in the graph diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx index ea29a288e6..e4d84ca059 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; import { GaugeCard } from './GaugeCard'; @@ -40,7 +40,12 @@ describe('', () => { it('handles invalid numbers', async () => { const badProps = { title: 'Tingle upgrade', progress: 'hejjo' } as any; - const { getByText } = await renderInTestApp(); - expect(getByText(/N\/A.*/)).toBeInTheDocument(); + const { error } = await withLogCollector(async () => { + const { getByText } = await renderInTestApp(); + expect(getByText(/N\/A.*/)).toBeInTheDocument(); + }); + expect(error).toEqual([ + expect.stringMatching(/^Warning: `NaN` is an invalid value/), + ]); }); }); diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx index 2501785701..f9348ffa9a 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx @@ -58,6 +58,7 @@ describe('', () => { const TextualBadge = React.forwardRef((props, ref) => ( diff --git a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx index 3d9dfe70a1..9a7a8ac47d 100644 --- a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx +++ b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; import { HomepageTimer } from './HomepageTimer'; import React from 'react'; import { lightTheme } from '@backstage/theme'; @@ -35,13 +39,18 @@ it('changes default timezone to GMT', async () => { context: 'test', }); - const rendered = await renderWithEffects( - - - - - , - ); + const { warn } = await withLogCollector(async () => { + const rendered = await renderWithEffects( + + + + + , + ); - expect(rendered.getByText('GMT')).toBeInTheDocument(); + expect(rendered.getByText('GMT')).toBeInTheDocument(); + }); + expect(warn).toEqual([ + 'The timezone America/New_Pork is invalid. Defaulting to GMT', + ]); }); diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index dcc92d9d47..0601c01a17 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -61,11 +61,19 @@ describe('extensions', () => { const Component = () =>
; const routeRef = createRouteRef({ id: 'foo' }); - const extension1 = createComponentExtension({ - component: { - sync: Component, - }, + let extension1: ReturnType; + const { warn } = withLogCollector(['warn'], () => { + extension1 = createComponentExtension({ + component: { + sync: Component, + }, + }); }); + expect(warn).toEqual([ + expect.stringMatching( + /^Declaring extensions without name is DEPRECATED. /, + ), + ]); const extension2 = createRoutableExtension({ name: 'Extension2', @@ -73,7 +81,7 @@ describe('extensions', () => { mountPoint: routeRef, }); - const ExtensionComponent1 = plugin.provide(extension1); + const ExtensionComponent1 = plugin.provide(extension1!); const ExtensionComponent2 = plugin.provide(extension2); const element1 = ; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 6a312436bf..d2efe81b90 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -19,6 +19,7 @@ import mockFs from 'mock-fs'; import child_process from 'child_process'; import path from 'path'; import { + Task, buildAppTask, checkAppExistsTask, checkPathExistsTask, @@ -27,6 +28,13 @@ import { templatingTask, } from './tasks'; +jest.spyOn(Task, 'log').mockReturnValue(undefined); +jest.spyOn(Task, 'error').mockReturnValue(undefined); +jest.spyOn(Task, 'section').mockReturnValue(undefined); +jest + .spyOn(Task, 'forItem') + .mockImplementation((_a, _b, taskFunc) => taskFunc()); + jest.mock('child_process'); // By mocking this the filesystem mocks won't mess with reading all of the package.jsons diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index fe2e568b5a..9bf979f6a6 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -54,6 +54,8 @@ catalog: # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter # This and userGroupMemberFilter are mutually exclusive, only one can be specified filter: accountEnabled eq true and userType eq 'member' + # See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0 + select: ['id', 'displayName', 'description'] # Optional configuration block userGroupMember: # Optional filter for users, use group membership to get users. diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 8f773e1b90..8b77ddf7c3 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -193,6 +193,7 @@ export type MicrosoftGraphProviderConfig = { clientId?: string; clientSecret?: string; userFilter?: string; + userSelect?: string[]; userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; @@ -232,6 +233,7 @@ export function readMicrosoftGraphOrg( options: { userExpand?: string; userFilter?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index 8748df5974..df41b581f9 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -74,6 +74,12 @@ export interface Config { * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The fields to be fetched on query. + * + * E.g. ["id", "displayName", "description"] + */ + userSelect?: string[]; /** * The search criteria to apply to extract users by groups memberships. * diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index f7675acf42..2429b67c89 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -62,6 +62,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "accountEnabled eq true and userType eq 'member'" */ userFilter?: string; + /** + * The fields to be fetched on query. + * + * E.g. ["id", "displayName", "description"] + */ + userSelect?: string[]; /** * The "expand" argument to apply to users. * @@ -144,6 +150,7 @@ export function readMicrosoftGraphConfig( const userExpand = providerConfig.getOptionalString('userExpand'); const userFilter = providerConfig.getOptionalString('userFilter'); + const userSelect = providerConfig.getOptionalStringArray('userSelect'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', ); @@ -196,6 +203,7 @@ export function readMicrosoftGraphConfig( clientSecret, userExpand, userFilter, + userSelect, userGroupMemberFilter, userGroupMemberSearch, groupExpand, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 3f43807809..a537526a9e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -1002,6 +1002,12 @@ describe('read microsoft graph', () => { }; } + async function* getExampleUsersEmail() { + yield { + mail: 'user.name@example.com', + }; + } + async function getExampleUserProfile(userId: string) { return { id: userId, @@ -1109,6 +1115,34 @@ describe('read microsoft graph', () => { ); }); + it('should read users with userSelect', async () => { + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + + client.getUsers.mockImplementation(getExampleUsersEmail); + client.getUserPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + + await readMicrosoftGraphOrg(client, 'tenantid', { + logger: getVoidLogger(), + userSelect: ['mail'], + }); + + expect(client.getUsers).toHaveBeenCalledTimes(1); + expect(client.getUsers).toHaveBeenCalledWith( + { + select: ['mail'], + }, + undefined, + ); + }); + it('should read users using userExpand and userGroupMemberFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index ecf74b34d9..8e1d9a3776 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -89,6 +89,7 @@ export async function readMicrosoftGraphUsers( queryMode?: 'basic' | 'advanced'; userFilter?: string; userExpand?: string; + userSelect?: string[]; transformer?: UserTransformer; logger: Logger; }, @@ -105,6 +106,7 @@ export async function readMicrosoftGraphUsers( { filter: options.userFilter, expand: options.userExpand, + select: options.userSelect, }, options.queryMode, )) { @@ -145,6 +147,7 @@ export async function readMicrosoftGraphUsersInGroups( options: { queryMode?: 'basic' | 'advanced'; userExpand?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -534,6 +537,7 @@ export async function readMicrosoftGraphOrg( options: { userExpand?: string; userFilter?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -565,6 +569,7 @@ export async function readMicrosoftGraphOrg( const { users: usersWithFilter } = await readMicrosoftGraphUsers(client, { queryMode: options.queryMode, userFilter: options.userFilter, + userSelect: options.userSelect, userExpand: options.userExpand, transformer: options.userTransformer, logger: options.logger, diff --git a/plugins/catalog-node/api-report.md b/plugins/catalog-node/api-report.md index 3873e3376d..525516be54 100644 --- a/plugins/catalog-node/api-report.md +++ b/plugins/catalog-node/api-report.md @@ -109,7 +109,7 @@ export type CatalogProcessorResult = | CatalogProcessorRefreshKeysResult; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // @public export type DeferredEntity = { diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index ddfff75462..c6a8fb44f5 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -31,13 +31,11 @@ export const catalogServiceRef = createServiceRef({ createServiceFactory({ service, deps: { - discoveryFactory: discoveryServiceRef, + discoveryApi: discoveryServiceRef, }, - factory: async ({ discoveryFactory }) => { - const discoveryApi = await discoveryFactory('root'); - const catalogClient = new CatalogClient({ discoveryApi }); - return async _pluginId => { - return catalogClient; + async factory() { + return async ({ discoveryApi }) => { + return new CatalogClient({ discoveryApi }); }; }, }), diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 1ae82f702b..ee43a0a5c1 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -436,6 +436,7 @@ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; + defaultNamespace?: string | false; }, ): string; diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index d41d3c6635..80ce2100f6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -34,6 +34,23 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:software'); }); + it('formats entity in default namespace without skipping default namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, { defaultNamespace: false }); + expect(title).toEqual('component:default/software'); + }); + it('formats entity in other namespace', () => { const entity = { apiVersion: 'v1', @@ -52,6 +69,24 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:test/software'); }); + it('formats entity in other namespace and hides this namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, { defaultNamespace: 'test' }); + expect(title).toEqual('component:software'); + }); + it('formats entity and hides default kind', () => { const entity = { apiVersion: 'v1', @@ -70,6 +105,27 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('test/software'); }); + it('formats entity and hides default kind and hiding namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, { + defaultKind: 'Component', + defaultNamespace: 'test', + }); + expect(title).toEqual('software'); + }); + it('formats entity name in default namespace', () => { const entityName = { kind: 'Component', @@ -80,6 +136,16 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:software'); }); + it('formats entity name in default namespace and does not skip default namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'default', + name: 'software', + }; + const title = humanizeEntityRef(entityName, { defaultNamespace: false }); + expect(title).toEqual('component:default/software'); + }); + it('formats entity name in other namespace', () => { const entityName = { kind: 'Component', @@ -91,6 +157,19 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:test/software'); }); + it('formats entity name in other namespace with skipping this namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, { + defaultNamespace: 'test', + }); + expect(title).toEqual('component:software'); + }); + it('renders link for entity name and hides default kind', () => { const entityName = { kind: 'Component', @@ -103,4 +182,31 @@ describe('humanizeEntityRef', () => { }); expect(title).toEqual('test/software'); }); + + it('renders link for entity name and hides default kind with skipping namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, { + defaultKind: 'component', + defaultNamespace: 'test', + }); + expect(title).toEqual('software'); + }); + + it('formats entity name in default namespace without skip of default namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'default', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, { + defaultNamespace: false, + }); + expect(title).toEqual('component:default/software'); + }); }); diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 2f72a7ee3b..bfc8e2af84 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -20,10 +20,17 @@ import { DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; -/** @public */ +/** + * @param defaultNamespace - if set to false then namespace is never omitted, + * if set to string which matches namespace of entity then omitted + * + * @public */ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, - opts?: { defaultKind?: string }, + opts?: { + defaultKind?: string; + defaultNamespace?: string | false; + }, ) { const defaultKind = opts?.defaultKind; let kind; @@ -40,7 +47,14 @@ export function humanizeEntityRef( name = entityRef.name; } - if (namespace === DEFAULT_NAMESPACE) { + if (namespace === undefined || namespace === '') { + namespace = DEFAULT_NAMESPACE; + } + if (opts?.defaultNamespace !== undefined) { + if (opts?.defaultNamespace === namespace) { + namespace = undefined; + } + } else if (namespace === DEFAULT_NAMESPACE) { namespace = undefined; } diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index d611309df3..9541098934 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -76,8 +76,12 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< // @public export interface EntityTagsPickerUiOptions { + // (undocumented) + helperText?: string; // (undocumented) kinds?: string[]; + // (undocumented) + showCounts?: boolean; } // @public diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index 5cf98cb8aa..c0e3aab436 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -16,8 +16,8 @@ import React, { useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { GetEntitiesRequest } from '@backstage/catalog-client'; -import { Entity, makeValidator } from '@backstage/catalog-model'; +import { GetEntityFacetsRequest } from '@backstage/catalog-client'; +import { makeValidator } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; @@ -32,6 +32,8 @@ import { FieldExtensionComponentProps } from '../../../extensions'; */ export interface EntityTagsPickerUiOptions { kinds?: string[]; + showCounts?: boolean; + helperText?: string; } /** @@ -45,26 +47,34 @@ export const EntityTagsPicker = ( ) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); + const [tagOptions, setTagOptions] = useState([]); const [inputValue, setInputValue] = useState(''); const [inputError, setInputError] = useState(false); const tagValidator = makeValidator().isValidTag; const kinds = uiSchema['ui:options']?.kinds; + const showCounts = uiSchema['ui:options']?.showCounts; + const helperText = uiSchema['ui:options']?.helperText; const { loading, value: existingTags } = useAsync(async () => { - const tagsRequest: GetEntitiesRequest = { fields: ['metadata.tags'] }; + const facet = 'metadata.tags'; + const tagsRequest: GetEntityFacetsRequest = { facets: [facet] }; if (kinds) { tagsRequest.filter = { kind: kinds }; } - const entities = await catalogApi.getEntities(tagsRequest); + const { facets } = await catalogApi.getEntityFacets(tagsRequest); - return [ - ...new Set( - entities.items - .flatMap((e: Entity) => e.metadata?.tags) - .filter(Boolean) as string[], + const tagFacets = Object.fromEntries( + facets[facet].map(({ value, count }) => [value, count]), + ); + + setTagOptions( + Object.keys(tagFacets).sort((a, b) => + showCounts ? tagFacets[b] - tagFacets[a] : a.localeCompare(b), ), - ].sort(); + ); + + return tagFacets; }); const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => { @@ -102,15 +112,21 @@ export const EntityTagsPicker = ( value={formData || []} inputValue={inputValue} loading={loading} - options={existingTags || []} + options={tagOptions} ChipProps={{ size: 'small' }} + renderOption={option => + showCounts ? `${option} (${existingTags?.[option]})` : option + } renderInput={params => ( setInputValue(e.target.value)} error={inputError} - helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters" + helperText={ + helperText ?? + "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters" + } /> )} /> diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 3592f58d4b..0bcfa0c48a 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2866,126 +2866,126 @@ __metadata: languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm-eabi@npm:1.2.247" +"@swc/core-android-arm-eabi@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm-eabi@npm:1.2.249" dependencies: "@swc/wasm": 1.2.122 conditions: os=android & cpu=arm languageName: node linkType: hard -"@swc/core-android-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm64@npm:1.2.247" +"@swc/core-android-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-arm64@npm:1.2.247" +"@swc/core-darwin-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-arm64@npm:1.2.249" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-x64@npm:1.2.247" +"@swc/core-darwin-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-x64@npm:1.2.249" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-freebsd-x64@npm:1.2.247" +"@swc/core-freebsd-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-freebsd-x64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247" +"@swc/core-linux-arm-gnueabihf@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247" +"@swc/core-linux-arm64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-musl@npm:1.2.247" +"@swc/core-linux-arm64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-musl@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-gnu@npm:1.2.247" +"@swc/core-linux-x64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-gnu@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-musl@npm:1.2.247" +"@swc/core-linux-x64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-musl@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247" +"@swc/core-win32-arm64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247" +"@swc/core-win32-ia32-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-x64-msvc@npm:1.2.247" +"@swc/core-win32-x64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-x64-msvc@npm:1.2.249" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.2.239": - version: 1.2.247 - resolution: "@swc/core@npm:1.2.247" + version: 1.2.249 + resolution: "@swc/core@npm:1.2.249" dependencies: - "@swc/core-android-arm-eabi": 1.2.247 - "@swc/core-android-arm64": 1.2.247 - "@swc/core-darwin-arm64": 1.2.247 - "@swc/core-darwin-x64": 1.2.247 - "@swc/core-freebsd-x64": 1.2.247 - "@swc/core-linux-arm-gnueabihf": 1.2.247 - "@swc/core-linux-arm64-gnu": 1.2.247 - "@swc/core-linux-arm64-musl": 1.2.247 - "@swc/core-linux-x64-gnu": 1.2.247 - "@swc/core-linux-x64-musl": 1.2.247 - "@swc/core-win32-arm64-msvc": 1.2.247 - "@swc/core-win32-ia32-msvc": 1.2.247 - "@swc/core-win32-x64-msvc": 1.2.247 + "@swc/core-android-arm-eabi": 1.2.249 + "@swc/core-android-arm64": 1.2.249 + "@swc/core-darwin-arm64": 1.2.249 + "@swc/core-darwin-x64": 1.2.249 + "@swc/core-freebsd-x64": 1.2.249 + "@swc/core-linux-arm-gnueabihf": 1.2.249 + "@swc/core-linux-arm64-gnu": 1.2.249 + "@swc/core-linux-arm64-musl": 1.2.249 + "@swc/core-linux-x64-gnu": 1.2.249 + "@swc/core-linux-x64-musl": 1.2.249 + "@swc/core-win32-arm64-msvc": 1.2.249 + "@swc/core-win32-ia32-msvc": 1.2.249 + "@swc/core-win32-x64-msvc": 1.2.249 dependenciesMeta: "@swc/core-android-arm-eabi": optional: true @@ -3015,7 +3015,7 @@ __metadata: optional: true bin: swcx: run_swcx.js - checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805 + checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 5939d370a2..c772e1a835 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13047,126 +13047,126 @@ __metadata: languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm-eabi@npm:1.2.247" +"@swc/core-android-arm-eabi@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm-eabi@npm:1.2.249" dependencies: "@swc/wasm": 1.2.122 conditions: os=android & cpu=arm languageName: node linkType: hard -"@swc/core-android-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm64@npm:1.2.247" +"@swc/core-android-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-arm64@npm:1.2.247" +"@swc/core-darwin-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-arm64@npm:1.2.249" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-x64@npm:1.2.247" +"@swc/core-darwin-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-x64@npm:1.2.249" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-freebsd-x64@npm:1.2.247" +"@swc/core-freebsd-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-freebsd-x64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247" +"@swc/core-linux-arm-gnueabihf@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247" +"@swc/core-linux-arm64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-musl@npm:1.2.247" +"@swc/core-linux-arm64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-musl@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-gnu@npm:1.2.247" +"@swc/core-linux-x64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-gnu@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-musl@npm:1.2.247" +"@swc/core-linux-x64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-musl@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247" +"@swc/core-win32-arm64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247" +"@swc/core-win32-ia32-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-x64-msvc@npm:1.2.247" +"@swc/core-win32-x64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-x64-msvc@npm:1.2.249" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.2.239": - version: 1.2.247 - resolution: "@swc/core@npm:1.2.247" + version: 1.2.249 + resolution: "@swc/core@npm:1.2.249" dependencies: - "@swc/core-android-arm-eabi": 1.2.247 - "@swc/core-android-arm64": 1.2.247 - "@swc/core-darwin-arm64": 1.2.247 - "@swc/core-darwin-x64": 1.2.247 - "@swc/core-freebsd-x64": 1.2.247 - "@swc/core-linux-arm-gnueabihf": 1.2.247 - "@swc/core-linux-arm64-gnu": 1.2.247 - "@swc/core-linux-arm64-musl": 1.2.247 - "@swc/core-linux-x64-gnu": 1.2.247 - "@swc/core-linux-x64-musl": 1.2.247 - "@swc/core-win32-arm64-msvc": 1.2.247 - "@swc/core-win32-ia32-msvc": 1.2.247 - "@swc/core-win32-x64-msvc": 1.2.247 + "@swc/core-android-arm-eabi": 1.2.249 + "@swc/core-android-arm64": 1.2.249 + "@swc/core-darwin-arm64": 1.2.249 + "@swc/core-darwin-x64": 1.2.249 + "@swc/core-freebsd-x64": 1.2.249 + "@swc/core-linux-arm-gnueabihf": 1.2.249 + "@swc/core-linux-arm64-gnu": 1.2.249 + "@swc/core-linux-arm64-musl": 1.2.249 + "@swc/core-linux-x64-gnu": 1.2.249 + "@swc/core-linux-x64-musl": 1.2.249 + "@swc/core-win32-arm64-msvc": 1.2.249 + "@swc/core-win32-ia32-msvc": 1.2.249 + "@swc/core-win32-x64-msvc": 1.2.249 dependenciesMeta: "@swc/core-android-arm-eabi": optional: true @@ -13196,7 +13196,7 @@ __metadata: optional: true bin: swcx: run_swcx.js - checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805 + checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268 languageName: node linkType: hard