@backstage/frontend-defaults 0.1.7-next.1 → 0.2.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @backstage/frontend-defaults
2
2
 
3
+ ## 0.2.0-next.2
4
+
5
+ ### Minor Changes
6
+
7
+ - 8250ffe: **BREAKING**: Dropped support for the removed opaque `@backstage/ExtensionOverrides` and `@backstage/BackstagePlugin` types.
8
+
9
+ ### Patch Changes
10
+
11
+ - 4d18b55: It's now possible to provide a middleware that wraps all extension factories by passing an `extensionFactoryMiddleware` to either `createApp()` or `createSpecializedApp()`.
12
+ - abcdf44: Internal refactor to match updated `createSpecializedApp`.
13
+ - e3f19db: Feature discovery and resolution logic used in `createApp` is now exposed via the `discoverAvailableFeatures` and `resolveAsyncFeatures` functions respectively.
14
+ - Updated dependencies
15
+ - @backstage/frontend-app-api@0.11.0-next.2
16
+ - @backstage/frontend-plugin-api@0.10.0-next.2
17
+ - @backstage/plugin-app@0.1.7-next.2
18
+ - @backstage/config@1.3.2
19
+ - @backstage/errors@1.2.7
20
+
3
21
  ## 0.1.7-next.1
4
22
 
5
23
  ### Patch Changes
@@ -1,11 +1,12 @@
1
1
  import React from 'react';
2
- import { stringifyError } from '@backstage/errors';
2
+ import { coreExtensionData } from '@backstage/frontend-plugin-api';
3
3
  import { defaultConfigLoaderSync } from './core-app-api/src/app/defaultConfigLoader.esm.js';
4
4
  import { overrideBaseUrlConfigs } from './core-app-api/src/app/overrideBaseUrlConfigs.esm.js';
5
- import { getAvailableFeatures } from './discovery.esm.js';
6
5
  import { ConfigReader } from '@backstage/config';
7
- import appPlugin from '@backstage/plugin-app';
8
6
  import { createSpecializedApp } from '@backstage/frontend-app-api';
7
+ import appPlugin from '@backstage/plugin-app';
8
+ import { discoverAvailableFeatures } from './discovery.esm.js';
9
+ import { resolveAsyncFeatures } from './resolution.esm.js';
9
10
 
10
11
  function createApp(options) {
11
12
  let suspenseFallback = options?.loadingComponent;
@@ -16,30 +17,21 @@ function createApp(options) {
16
17
  const config = await options?.configLoader?.().then((c) => c.config) ?? ConfigReader.fromConfigs(
17
18
  overrideBaseUrlConfigs(defaultConfigLoaderSync())
18
19
  );
19
- const discoveredFeatures = getAvailableFeatures(config);
20
- const providedFeatures = [];
21
- for (const entry of options?.features ?? []) {
22
- if ("load" in entry) {
23
- try {
24
- const result = await entry.load({ config });
25
- providedFeatures.push(...result.features);
26
- } catch (e) {
27
- throw new Error(
28
- `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
29
- e
30
- )}`
31
- );
32
- }
33
- } else {
34
- providedFeatures.push(entry);
35
- }
36
- }
20
+ const { features: discoveredFeatures } = discoverAvailableFeatures(config);
21
+ const { features: providedFeatures } = await resolveAsyncFeatures({
22
+ config,
23
+ features: options?.features
24
+ });
37
25
  const app = createSpecializedApp({
38
26
  config,
39
27
  features: [appPlugin, ...discoveredFeatures, ...providedFeatures],
40
- bindRoutes: options?.bindRoutes
41
- }).createRoot();
42
- return { default: () => app };
28
+ bindRoutes: options?.bindRoutes,
29
+ extensionFactoryMiddleware: options?.extensionFactoryMiddleware
30
+ });
31
+ const rootEl = app.tree.root.instance.getData(
32
+ coreExtensionData.reactElement
33
+ );
34
+ return { default: () => rootEl };
43
35
  }
44
36
  return {
45
37
  createRoot() {
@@ -1 +1 @@
1
- {"version":3,"file":"createApp.esm.js","sources":["../src/createApp.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React, { JSX, ReactNode } from 'react';\nimport { ConfigApi } from '@backstage/frontend-plugin-api';\nimport { stringifyError } from '@backstage/errors';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs';\nimport { getAvailableFeatures } from './discovery';\nimport { ConfigReader } from '@backstage/config';\nimport appPlugin from '@backstage/plugin-app';\nimport {\n CreateAppRouteBinder,\n FrontendFeature,\n createSpecializedApp,\n} from '@backstage/frontend-app-api';\n\n/**\n * A source of dynamically loaded frontend features.\n *\n * @public\n */\nexport interface CreateAppFeatureLoader {\n /**\n * Returns name of this loader. suitable for showing to users.\n */\n getLoaderName(): string;\n\n /**\n * Loads a number of features dynamically.\n */\n load(options: { config: ConfigApi }): Promise<{\n features: FrontendFeature[];\n }>;\n}\n\n/**\n * Options for {@link createApp}.\n *\n * @public\n */\nexport interface CreateAppOptions {\n features?: (FrontendFeature | CreateAppFeatureLoader)[];\n configLoader?: () => Promise<{ config: ConfigApi }>;\n bindRoutes?(context: { bind: CreateAppRouteBinder }): void;\n /**\n * The component to render while loading the app (waiting for config, features, etc)\n *\n * Is the text \"Loading...\" by default.\n * If set to \"null\" then no loading fallback component is rendered. *\n */\n loadingComponent?: ReactNode;\n}\n\n/**\n * Creates a new Backstage frontend app instance. See https://backstage.io/docs/frontend-system/building-apps/index\n *\n * @public\n */\nexport function createApp(options?: CreateAppOptions): {\n createRoot(): JSX.Element;\n} {\n let suspenseFallback = options?.loadingComponent;\n if (suspenseFallback === undefined) {\n suspenseFallback = 'Loading...';\n }\n\n async function appLoader() {\n const config =\n (await options?.configLoader?.().then(c => c.config)) ??\n ConfigReader.fromConfigs(\n overrideBaseUrlConfigs(defaultConfigLoaderSync()),\n );\n\n const discoveredFeatures = getAvailableFeatures(config);\n\n const providedFeatures: FrontendFeature[] = [];\n for (const entry of options?.features ?? []) {\n if ('load' in entry) {\n try {\n const result = await entry.load({ config });\n providedFeatures.push(...result.features);\n } catch (e) {\n throw new Error(\n `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(\n e,\n )}`,\n );\n }\n } else {\n providedFeatures.push(entry);\n }\n }\n\n const app = createSpecializedApp({\n config,\n features: [appPlugin, ...discoveredFeatures, ...providedFeatures],\n bindRoutes: options?.bindRoutes,\n }).createRoot();\n\n return { default: () => app };\n }\n\n return {\n createRoot() {\n const LazyApp = React.lazy(appLoader);\n return (\n <React.Suspense fallback={suspenseFallback}>\n <LazyApp />\n </React.Suspense>\n );\n },\n };\n}\n"],"names":[],"mappings":";;;;;;;;;AA0EO,SAAS,UAAU,OAExB,EAAA;AACA,EAAA,IAAI,mBAAmB,OAAS,EAAA,gBAAA;AAChC,EAAA,IAAI,qBAAqB,KAAW,CAAA,EAAA;AAClC,IAAmB,gBAAA,GAAA,YAAA;AAAA;AAGrB,EAAA,eAAe,SAAY,GAAA;AACzB,IAAM,MAAA,MAAA,GACH,MAAM,OAAA,EAAS,YAAe,IAAA,CAAE,KAAK,CAAK,CAAA,KAAA,CAAA,CAAE,MAAM,CAAA,IACnD,YAAa,CAAA,WAAA;AAAA,MACX,sBAAA,CAAuB,yBAAyB;AAAA,KAClD;AAEF,IAAM,MAAA,kBAAA,GAAqB,qBAAqB,MAAM,CAAA;AAEtD,IAAA,MAAM,mBAAsC,EAAC;AAC7C,IAAA,KAAA,MAAW,KAAS,IAAA,OAAA,EAAS,QAAY,IAAA,EAAI,EAAA;AAC3C,MAAA,IAAI,UAAU,KAAO,EAAA;AACnB,QAAI,IAAA;AACF,UAAA,MAAM,SAAS,MAAM,KAAA,CAAM,IAAK,CAAA,EAAE,QAAQ,CAAA;AAC1C,UAAiB,gBAAA,CAAA,IAAA,CAAK,GAAG,MAAA,CAAO,QAAQ,CAAA;AAAA,iBACjC,CAAG,EAAA;AACV,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,CAAiD,8CAAA,EAAA,KAAA,CAAM,aAAc,EAAC,CAAM,GAAA,EAAA,cAAA;AAAA,cAC1E;AAAA,aACD,CAAA;AAAA,WACH;AAAA;AACF,OACK,MAAA;AACL,QAAA,gBAAA,CAAiB,KAAK,KAAK,CAAA;AAAA;AAC7B;AAGF,IAAA,MAAM,MAAM,oBAAqB,CAAA;AAAA,MAC/B,MAAA;AAAA,MACA,UAAU,CAAC,SAAA,EAAW,GAAG,kBAAA,EAAoB,GAAG,gBAAgB,CAAA;AAAA,MAChE,YAAY,OAAS,EAAA;AAAA,KACtB,EAAE,UAAW,EAAA;AAEd,IAAO,OAAA,EAAE,OAAS,EAAA,MAAM,GAAI,EAAA;AAAA;AAG9B,EAAO,OAAA;AAAA,IACL,UAAa,GAAA;AACX,MAAM,MAAA,OAAA,GAAU,KAAM,CAAA,IAAA,CAAK,SAAS,CAAA;AACpC,MACE,uBAAA,KAAA,CAAA,aAAA,CAAC,MAAM,QAAN,EAAA,EAAe,UAAU,gBACxB,EAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,aAAQ,CACX,CAAA;AAAA;AAEJ,GACF;AACF;;;;"}
1
+ {"version":3,"file":"createApp.esm.js","sources":["../src/createApp.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React, { JSX, ReactNode } from 'react';\nimport {\n ConfigApi,\n coreExtensionData,\n ExtensionFactoryMiddleware,\n} from '@backstage/frontend-plugin-api';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs';\nimport { ConfigReader } from '@backstage/config';\nimport {\n CreateAppRouteBinder,\n FrontendFeature,\n createSpecializedApp,\n} from '@backstage/frontend-app-api';\nimport appPlugin from '@backstage/plugin-app';\nimport { discoverAvailableFeatures } from './discovery';\nimport { resolveAsyncFeatures } from './resolution';\n\n/**\n * A source of dynamically loaded frontend features.\n *\n * @public\n */\nexport interface CreateAppFeatureLoader {\n /**\n * Returns name of this loader. suitable for showing to users.\n */\n getLoaderName(): string;\n\n /**\n * Loads a number of features dynamically.\n */\n load(options: { config: ConfigApi }): Promise<{\n features: FrontendFeature[];\n }>;\n}\n\n/**\n * Options for {@link createApp}.\n *\n * @public\n */\nexport interface CreateAppOptions {\n features?: (FrontendFeature | CreateAppFeatureLoader)[];\n configLoader?: () => Promise<{ config: ConfigApi }>;\n bindRoutes?(context: { bind: CreateAppRouteBinder }): void;\n /**\n * The component to render while loading the app (waiting for config, features, etc)\n *\n * Is the text \"Loading...\" by default.\n * If set to \"null\" then no loading fallback component is rendered. *\n */\n loadingComponent?: ReactNode;\n extensionFactoryMiddleware?:\n | ExtensionFactoryMiddleware\n | ExtensionFactoryMiddleware[];\n}\n\n/**\n * Creates a new Backstage frontend app instance. See https://backstage.io/docs/frontend-system/building-apps/index\n *\n * @public\n */\nexport function createApp(options?: CreateAppOptions): {\n createRoot(): JSX.Element;\n} {\n let suspenseFallback = options?.loadingComponent;\n if (suspenseFallback === undefined) {\n suspenseFallback = 'Loading...';\n }\n\n async function appLoader() {\n const config =\n (await options?.configLoader?.().then(c => c.config)) ??\n ConfigReader.fromConfigs(\n overrideBaseUrlConfigs(defaultConfigLoaderSync()),\n );\n\n const { features: discoveredFeatures } = discoverAvailableFeatures(config);\n const { features: providedFeatures } = await resolveAsyncFeatures({\n config,\n features: options?.features,\n });\n\n const app = createSpecializedApp({\n config,\n features: [appPlugin, ...discoveredFeatures, ...providedFeatures],\n bindRoutes: options?.bindRoutes,\n extensionFactoryMiddleware: options?.extensionFactoryMiddleware,\n });\n\n const rootEl = app.tree.root.instance!.getData(\n coreExtensionData.reactElement,\n );\n\n return { default: () => rootEl };\n }\n\n return {\n createRoot() {\n const LazyApp = React.lazy(appLoader);\n return (\n <React.Suspense fallback={suspenseFallback}>\n <LazyApp />\n </React.Suspense>\n );\n },\n };\n}\n"],"names":[],"mappings":";;;;;;;;;;AAiFO,SAAS,UAAU,OAExB,EAAA;AACA,EAAA,IAAI,mBAAmB,OAAS,EAAA,gBAAA;AAChC,EAAA,IAAI,qBAAqB,KAAW,CAAA,EAAA;AAClC,IAAmB,gBAAA,GAAA,YAAA;AAAA;AAGrB,EAAA,eAAe,SAAY,GAAA;AACzB,IAAM,MAAA,MAAA,GACH,MAAM,OAAA,EAAS,YAAe,IAAA,CAAE,KAAK,CAAK,CAAA,KAAA,CAAA,CAAE,MAAM,CAAA,IACnD,YAAa,CAAA,WAAA;AAAA,MACX,sBAAA,CAAuB,yBAAyB;AAAA,KAClD;AAEF,IAAA,MAAM,EAAE,QAAA,EAAU,kBAAmB,EAAA,GAAI,0BAA0B,MAAM,CAAA;AACzE,IAAA,MAAM,EAAE,QAAA,EAAU,gBAAiB,EAAA,GAAI,MAAM,oBAAqB,CAAA;AAAA,MAChE,MAAA;AAAA,MACA,UAAU,OAAS,EAAA;AAAA,KACpB,CAAA;AAED,IAAA,MAAM,MAAM,oBAAqB,CAAA;AAAA,MAC/B,MAAA;AAAA,MACA,UAAU,CAAC,SAAA,EAAW,GAAG,kBAAA,EAAoB,GAAG,gBAAgB,CAAA;AAAA,MAChE,YAAY,OAAS,EAAA,UAAA;AAAA,MACrB,4BAA4B,OAAS,EAAA;AAAA,KACtC,CAAA;AAED,IAAA,MAAM,MAAS,GAAA,GAAA,CAAI,IAAK,CAAA,IAAA,CAAK,QAAU,CAAA,OAAA;AAAA,MACrC,iBAAkB,CAAA;AAAA,KACpB;AAEA,IAAO,OAAA,EAAE,OAAS,EAAA,MAAM,MAAO,EAAA;AAAA;AAGjC,EAAO,OAAA;AAAA,IACL,UAAa,GAAA;AACX,MAAM,MAAA,OAAA,GAAU,KAAM,CAAA,IAAA,CAAK,SAAS,CAAA;AACpC,MACE,uBAAA,KAAA,CAAA,aAAA,CAAC,MAAM,QAAN,EAAA,EAAe,UAAU,gBACxB,EAAA,kBAAA,KAAA,CAAA,aAAA,CAAC,aAAQ,CACX,CAAA;AAAA;AAEJ,GACF;AACF;;;;"}
@@ -27,30 +27,30 @@ function readPackageDetectionConfig(config) {
27
27
  exclude: packagesConfig.getOptionalStringArray("exclude")
28
28
  };
29
29
  }
30
- function getAvailableFeatures(config) {
30
+ function discoverAvailableFeatures(config) {
31
31
  const discovered = window["__@backstage/discovered__"];
32
32
  const detection = readPackageDetectionConfig(config);
33
33
  if (!detection) {
34
- return [];
34
+ return { features: [] };
35
35
  }
36
- return discovered?.modules.filter(({ name }) => {
37
- if (detection.exclude?.includes(name)) {
38
- return false;
39
- }
40
- if (detection.include && !detection.include.includes(name)) {
41
- return false;
42
- }
43
- return true;
44
- }).map((m) => m.default).filter(isBackstageFeature) ?? [];
36
+ return {
37
+ features: discovered?.modules.filter(({ name }) => {
38
+ if (detection.exclude?.includes(name)) {
39
+ return false;
40
+ }
41
+ if (detection.include && !detection.include.includes(name)) {
42
+ return false;
43
+ }
44
+ return true;
45
+ }).map((m) => m.default).filter(isBackstageFeature) ?? []
46
+ };
45
47
  }
46
48
  function isBackstageFeature(obj) {
47
49
  if (obj !== null && typeof obj === "object" && "$$type" in obj) {
48
- return obj.$$type === "@backstage/FrontendPlugin" || obj.$$type === "@backstage/FrontendModule" || // TODO: Remove this once the old plugin type and extension overrides
49
- // are no longer supported
50
- obj.$$type === "@backstage/BackstagePlugin" || obj.$$type === "@backstage/ExtensionOverrides";
50
+ return obj.$$type === "@backstage/FrontendPlugin" || obj.$$type === "@backstage/FrontendModule";
51
51
  }
52
52
  return false;
53
53
  }
54
54
 
55
- export { getAvailableFeatures };
55
+ export { discoverAvailableFeatures };
56
56
  //# sourceMappingURL=discovery.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.esm.js","sources":["../src/discovery.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config, ConfigReader } from '@backstage/config';\nimport { FrontendFeature } from '@backstage/frontend-app-api';\n\ninterface DiscoveryGlobal {\n modules: Array<{ name: string; export?: string; default: unknown }>;\n}\n\nfunction readPackageDetectionConfig(config: Config) {\n const packages = config.getOptional('app.experimental.packages');\n if (packages === undefined || packages === null) {\n return undefined;\n }\n\n if (typeof packages === 'string') {\n if (packages !== 'all') {\n throw new Error(\n `Invalid app.experimental.packages mode, got '${packages}', expected 'all'`,\n );\n }\n return {};\n }\n\n if (typeof packages !== 'object' || Array.isArray(packages)) {\n throw new Error(\n \"Invalid config at 'app.experimental.packages', expected object\",\n );\n }\n const packagesConfig = new ConfigReader(\n packages,\n 'app.experimental.packages',\n );\n\n return {\n include: packagesConfig.getOptionalStringArray('include'),\n exclude: packagesConfig.getOptionalStringArray('exclude'),\n };\n}\n\n/**\n * @internal\n */\nexport function getAvailableFeatures(config: Config): FrontendFeature[] {\n const discovered = (\n window as { '__@backstage/discovered__'?: DiscoveryGlobal }\n )['__@backstage/discovered__'];\n\n const detection = readPackageDetectionConfig(config);\n if (!detection) {\n return [];\n }\n\n return (\n discovered?.modules\n .filter(({ name }) => {\n if (detection.exclude?.includes(name)) {\n return false;\n }\n if (detection.include && !detection.include.includes(name)) {\n return false;\n }\n return true;\n })\n .map(m => m.default)\n .filter(isBackstageFeature) ?? []\n );\n}\n\nfunction isBackstageFeature(obj: unknown): obj is FrontendFeature {\n if (obj !== null && typeof obj === 'object' && '$$type' in obj) {\n return (\n obj.$$type === '@backstage/FrontendPlugin' ||\n obj.$$type === '@backstage/FrontendModule' ||\n // TODO: Remove this once the old plugin type and extension overrides\n // are no longer supported\n obj.$$type === '@backstage/BackstagePlugin' ||\n obj.$$type === '@backstage/ExtensionOverrides'\n );\n }\n return false;\n}\n"],"names":[],"mappings":";;AAuBA,SAAS,2BAA2B,MAAgB,EAAA;AAClD,EAAM,MAAA,QAAA,GAAW,MAAO,CAAA,WAAA,CAAY,2BAA2B,CAAA;AAC/D,EAAI,IAAA,QAAA,KAAa,KAAa,CAAA,IAAA,QAAA,KAAa,IAAM,EAAA;AAC/C,IAAO,OAAA,KAAA,CAAA;AAAA;AAGT,EAAI,IAAA,OAAO,aAAa,QAAU,EAAA;AAChC,IAAA,IAAI,aAAa,KAAO,EAAA;AACtB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,gDAAgD,QAAQ,CAAA,iBAAA;AAAA,OAC1D;AAAA;AAEF,IAAA,OAAO,EAAC;AAAA;AAGV,EAAA,IAAI,OAAO,QAAa,KAAA,QAAA,IAAY,KAAM,CAAA,OAAA,CAAQ,QAAQ,CAAG,EAAA;AAC3D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA;AAEF,EAAA,MAAM,iBAAiB,IAAI,YAAA;AAAA,IACzB,QAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAO,OAAA;AAAA,IACL,OAAA,EAAS,cAAe,CAAA,sBAAA,CAAuB,SAAS,CAAA;AAAA,IACxD,OAAA,EAAS,cAAe,CAAA,sBAAA,CAAuB,SAAS;AAAA,GAC1D;AACF;AAKO,SAAS,qBAAqB,MAAmC,EAAA;AACtE,EAAM,MAAA,UAAA,GACJ,OACA,2BAA2B,CAAA;AAE7B,EAAM,MAAA,SAAA,GAAY,2BAA2B,MAAM,CAAA;AACnD,EAAA,IAAI,CAAC,SAAW,EAAA;AACd,IAAA,OAAO,EAAC;AAAA;AAGV,EAAA,OACE,YAAY,OACT,CAAA,MAAA,CAAO,CAAC,EAAE,MAAW,KAAA;AACpB,IAAA,IAAI,SAAU,CAAA,OAAA,EAAS,QAAS,CAAA,IAAI,CAAG,EAAA;AACrC,MAAO,OAAA,KAAA;AAAA;AAET,IAAA,IAAI,UAAU,OAAW,IAAA,CAAC,UAAU,OAAQ,CAAA,QAAA,CAAS,IAAI,CAAG,EAAA;AAC1D,MAAO,OAAA,KAAA;AAAA;AAET,IAAO,OAAA,IAAA;AAAA,GACR,CACA,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,OAAO,CAClB,CAAA,MAAA,CAAO,kBAAkB,CAAA,IAAK,EAAC;AAEtC;AAEA,SAAS,mBAAmB,GAAsC,EAAA;AAChE,EAAA,IAAI,QAAQ,IAAQ,IAAA,OAAO,GAAQ,KAAA,QAAA,IAAY,YAAY,GAAK,EAAA;AAC9D,IAAA,OACE,GAAI,CAAA,MAAA,KAAW,2BACf,IAAA,GAAA,CAAI,MAAW,KAAA,2BAAA;AAAA;AAAA,IAGf,GAAI,CAAA,MAAA,KAAW,4BACf,IAAA,GAAA,CAAI,MAAW,KAAA,+BAAA;AAAA;AAGnB,EAAO,OAAA,KAAA;AACT;;;;"}
1
+ {"version":3,"file":"discovery.esm.js","sources":["../src/discovery.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config, ConfigReader } from '@backstage/config';\nimport { FrontendFeature } from '@backstage/frontend-app-api';\n\ninterface DiscoveryGlobal {\n modules: Array<{ name: string; export?: string; default: unknown }>;\n}\n\nfunction readPackageDetectionConfig(config: Config) {\n const packages = config.getOptional('app.experimental.packages');\n if (packages === undefined || packages === null) {\n return undefined;\n }\n\n if (typeof packages === 'string') {\n if (packages !== 'all') {\n throw new Error(\n `Invalid app.experimental.packages mode, got '${packages}', expected 'all'`,\n );\n }\n return {};\n }\n\n if (typeof packages !== 'object' || Array.isArray(packages)) {\n throw new Error(\n \"Invalid config at 'app.experimental.packages', expected object\",\n );\n }\n const packagesConfig = new ConfigReader(\n packages,\n 'app.experimental.packages',\n );\n\n return {\n include: packagesConfig.getOptionalStringArray('include'),\n exclude: packagesConfig.getOptionalStringArray('exclude'),\n };\n}\n\n/**\n * @public\n */\nexport function discoverAvailableFeatures(config: Config): {\n features: FrontendFeature[];\n} {\n const discovered = (\n window as { '__@backstage/discovered__'?: DiscoveryGlobal }\n )['__@backstage/discovered__'];\n\n const detection = readPackageDetectionConfig(config);\n if (!detection) {\n return { features: [] };\n }\n\n return {\n features:\n discovered?.modules\n .filter(({ name }) => {\n if (detection.exclude?.includes(name)) {\n return false;\n }\n if (detection.include && !detection.include.includes(name)) {\n return false;\n }\n return true;\n })\n .map(m => m.default)\n .filter(isBackstageFeature) ?? [],\n };\n}\n\nfunction isBackstageFeature(obj: unknown): obj is FrontendFeature {\n if (obj !== null && typeof obj === 'object' && '$$type' in obj) {\n return (\n obj.$$type === '@backstage/FrontendPlugin' ||\n obj.$$type === '@backstage/FrontendModule'\n );\n }\n return false;\n}\n"],"names":[],"mappings":";;AAuBA,SAAS,2BAA2B,MAAgB,EAAA;AAClD,EAAM,MAAA,QAAA,GAAW,MAAO,CAAA,WAAA,CAAY,2BAA2B,CAAA;AAC/D,EAAI,IAAA,QAAA,KAAa,KAAa,CAAA,IAAA,QAAA,KAAa,IAAM,EAAA;AAC/C,IAAO,OAAA,KAAA,CAAA;AAAA;AAGT,EAAI,IAAA,OAAO,aAAa,QAAU,EAAA;AAChC,IAAA,IAAI,aAAa,KAAO,EAAA;AACtB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,gDAAgD,QAAQ,CAAA,iBAAA;AAAA,OAC1D;AAAA;AAEF,IAAA,OAAO,EAAC;AAAA;AAGV,EAAA,IAAI,OAAO,QAAa,KAAA,QAAA,IAAY,KAAM,CAAA,OAAA,CAAQ,QAAQ,CAAG,EAAA;AAC3D,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA;AAEF,EAAA,MAAM,iBAAiB,IAAI,YAAA;AAAA,IACzB,QAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAO,OAAA;AAAA,IACL,OAAA,EAAS,cAAe,CAAA,sBAAA,CAAuB,SAAS,CAAA;AAAA,IACxD,OAAA,EAAS,cAAe,CAAA,sBAAA,CAAuB,SAAS;AAAA,GAC1D;AACF;AAKO,SAAS,0BAA0B,MAExC,EAAA;AACA,EAAM,MAAA,UAAA,GACJ,OACA,2BAA2B,CAAA;AAE7B,EAAM,MAAA,SAAA,GAAY,2BAA2B,MAAM,CAAA;AACnD,EAAA,IAAI,CAAC,SAAW,EAAA;AACd,IAAO,OAAA,EAAE,QAAU,EAAA,EAAG,EAAA;AAAA;AAGxB,EAAO,OAAA;AAAA,IACL,UACE,UAAY,EAAA,OAAA,CACT,OAAO,CAAC,EAAE,MAAW,KAAA;AACpB,MAAA,IAAI,SAAU,CAAA,OAAA,EAAS,QAAS,CAAA,IAAI,CAAG,EAAA;AACrC,QAAO,OAAA,KAAA;AAAA;AAET,MAAA,IAAI,UAAU,OAAW,IAAA,CAAC,UAAU,OAAQ,CAAA,QAAA,CAAS,IAAI,CAAG,EAAA;AAC1D,QAAO,OAAA,KAAA;AAAA;AAET,MAAO,OAAA,IAAA;AAAA,KACR,CACA,CAAA,GAAA,CAAI,CAAK,CAAA,KAAA,CAAA,CAAE,OAAO,CAClB,CAAA,MAAA,CAAO,kBAAkB,CAAA,IAAK;AAAC,GACtC;AACF;AAEA,SAAS,mBAAmB,GAAsC,EAAA;AAChE,EAAA,IAAI,QAAQ,IAAQ,IAAA,OAAO,GAAQ,KAAA,QAAA,IAAY,YAAY,GAAK,EAAA;AAC9D,IAAA,OACE,GAAI,CAAA,MAAA,KAAW,2BACf,IAAA,GAAA,CAAI,MAAW,KAAA,2BAAA;AAAA;AAGnB,EAAO,OAAA,KAAA;AACT;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { ReactNode, JSX } from 'react';
2
- import { ConfigApi } from '@backstage/frontend-plugin-api';
2
+ import { ConfigApi, ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api';
3
3
  import { FrontendFeature, CreateAppRouteBinder } from '@backstage/frontend-app-api';
4
+ import { Config } from '@backstage/config';
4
5
 
5
6
  /**
6
7
  * A source of dynamically loaded frontend features.
@@ -41,6 +42,7 @@ interface CreateAppOptions {
41
42
  * If set to "null" then no loading fallback component is rendered. *
42
43
  */
43
44
  loadingComponent?: ReactNode;
45
+ extensionFactoryMiddleware?: ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[];
44
46
  }
45
47
  /**
46
48
  * Creates a new Backstage frontend app instance. See https://backstage.io/docs/frontend-system/building-apps/index
@@ -75,4 +77,19 @@ declare function createPublicSignInApp(options?: CreateAppOptions): {
75
77
  createRoot(): React.JSX.Element;
76
78
  };
77
79
 
78
- export { type CreateAppFeatureLoader, type CreateAppOptions, createApp, createPublicSignInApp };
80
+ /**
81
+ * @public
82
+ */
83
+ declare function discoverAvailableFeatures(config: Config): {
84
+ features: FrontendFeature[];
85
+ };
86
+
87
+ /** @public */
88
+ declare function resolveAsyncFeatures(options: {
89
+ config: Config;
90
+ features?: (FrontendFeature | CreateAppFeatureLoader)[];
91
+ }): Promise<{
92
+ features: FrontendFeature[];
93
+ }>;
94
+
95
+ export { type CreateAppFeatureLoader, type CreateAppOptions, createApp, createPublicSignInApp, discoverAvailableFeatures, resolveAsyncFeatures };
package/dist/index.esm.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { createApp } from './createApp.esm.js';
2
2
  export { createPublicSignInApp } from './createPublicSignInApp.esm.js';
3
+ export { discoverAvailableFeatures } from './discovery.esm.js';
4
+ export { resolveAsyncFeatures } from './resolution.esm.js';
3
5
  //# sourceMappingURL=index.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
@@ -0,0 +1,25 @@
1
+ import { stringifyError } from '@backstage/errors';
2
+
3
+ async function resolveAsyncFeatures(options) {
4
+ const features = [];
5
+ for (const entry of options.features ?? []) {
6
+ if ("load" in entry) {
7
+ try {
8
+ const result = await entry.load({ config: options.config });
9
+ features.push(...result.features);
10
+ } catch (e) {
11
+ throw new Error(
12
+ `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
13
+ e
14
+ )}`
15
+ );
16
+ }
17
+ } else {
18
+ features.push(entry);
19
+ }
20
+ }
21
+ return { features };
22
+ }
23
+
24
+ export { resolveAsyncFeatures };
25
+ //# sourceMappingURL=resolution.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolution.esm.js","sources":["../src/resolution.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport { stringifyError } from '@backstage/errors';\nimport { FrontendFeature } from '@backstage/frontend-app-api';\nimport { CreateAppFeatureLoader } from './createApp';\n\n/** @public */\nexport async function resolveAsyncFeatures(options: {\n config: Config;\n features?: (FrontendFeature | CreateAppFeatureLoader)[];\n}): Promise<{ features: FrontendFeature[] }> {\n const features = [];\n for (const entry of options.features ?? []) {\n if ('load' in entry) {\n try {\n const result = await entry.load({ config: options.config });\n features.push(...result.features);\n } catch (e) {\n throw new Error(\n `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(\n e,\n )}`,\n );\n }\n } else {\n features.push(entry);\n }\n }\n return { features };\n}\n"],"names":[],"mappings":";;AAsBA,eAAsB,qBAAqB,OAGE,EAAA;AAC3C,EAAA,MAAM,WAAW,EAAC;AAClB,EAAA,KAAA,MAAW,KAAS,IAAA,OAAA,CAAQ,QAAY,IAAA,EAAI,EAAA;AAC1C,IAAA,IAAI,UAAU,KAAO,EAAA;AACnB,MAAI,IAAA;AACF,QAAM,MAAA,MAAA,GAAS,MAAM,KAAM,CAAA,IAAA,CAAK,EAAE,MAAQ,EAAA,OAAA,CAAQ,QAAQ,CAAA;AAC1D,QAAS,QAAA,CAAA,IAAA,CAAK,GAAG,MAAA,CAAO,QAAQ,CAAA;AAAA,eACzB,CAAG,EAAA;AACV,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAiD,8CAAA,EAAA,KAAA,CAAM,aAAc,EAAC,CAAM,GAAA,EAAA,cAAA;AAAA,YAC1E;AAAA,WACD,CAAA;AAAA,SACH;AAAA;AACF,KACK,MAAA;AACL,MAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AAAA;AACrB;AAEF,EAAA,OAAO,EAAE,QAAS,EAAA;AACpB;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/frontend-defaults",
3
- "version": "0.1.7-next.1",
3
+ "version": "0.2.0-next.2",
4
4
  "backstage": {
5
5
  "role": "web-library"
6
6
  },
@@ -33,15 +33,15 @@
33
33
  "dependencies": {
34
34
  "@backstage/config": "1.3.2",
35
35
  "@backstage/errors": "1.2.7",
36
- "@backstage/frontend-app-api": "0.10.6-next.1",
37
- "@backstage/frontend-plugin-api": "0.9.6-next.1",
38
- "@backstage/plugin-app": "0.1.7-next.1",
36
+ "@backstage/frontend-app-api": "0.11.0-next.2",
37
+ "@backstage/frontend-plugin-api": "0.10.0-next.2",
38
+ "@backstage/plugin-app": "0.1.7-next.2",
39
39
  "@react-hookz/web": "^24.0.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@backstage/cli": "0.30.1-next.0",
42
+ "@backstage/cli": "0.31.0-next.1",
43
43
  "@backstage/core-plugin-api": "1.10.4",
44
- "@backstage/test-utils": "1.7.5",
44
+ "@backstage/test-utils": "1.7.6-next.0",
45
45
  "@testing-library/jest-dom": "^6.0.0",
46
46
  "@testing-library/react": "^16.0.0",
47
47
  "@types/react": "^18.0.0",
@@ -60,12 +60,5 @@
60
60
  "optional": true
61
61
  }
62
62
  },
63
- "typesVersions": {
64
- "*": {
65
- "index": [
66
- "dist/index.d.ts"
67
- ]
68
- }
69
- },
70
63
  "module": "./dist/index.esm.js"
71
64
  }