@backstage/core-app-api 1.14.2 → 1.15.0

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,35 @@
1
1
  # @backstage/core-app-api
2
2
 
3
+ ## 1.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ddbeace: Added the ability to explicitly disable routes through the `bindRoutes` option by passing `false` as the route target. This also fixes a bug where route bindings in config were incorrectly prioritized above the ones in code in certain situations.
8
+
9
+ ### Patch Changes
10
+
11
+ - ea69e46: The `defaultConfigLoader` now also reads configuration from scripts tags with `type="backstage.io/config"`. The tag is expected to contain a JSON-serialized array of `AppConfig` objects. If any of these script tags are present, the injected runtime configuration in the static assets will no longer be used.
12
+ - b537bd7: Allow custom star icons to be provided via the `star` and `unstarred` icon overrides. See how to override existing icons in the [Backstage documentation](https://backstage.io/docs/getting-started/app-custom-theme/#custom-icons).
13
+ - 836127c: Updated dependency `@testing-library/react` to `^16.0.0`.
14
+ - Updated dependencies
15
+ - @backstage/core-plugin-api@1.9.4
16
+ - @backstage/version-bridge@1.0.9
17
+ - @backstage/config@1.2.0
18
+ - @backstage/types@1.1.1
19
+
20
+ ## 1.14.3-next.0
21
+
22
+ ### Patch Changes
23
+
24
+ - ea69e46: The `defaultConfigLoader` now also reads configuration from scripts tags with `type="backstage.io/config"`. The tag is expected to contain a JSON-serialized array of `AppConfig` objects. If any of these script tags are present, the injected runtime configuration in the static assets will no longer be used.
25
+ - b537bd7: Allow custom star icons to be provided via the `star` and `unstarred` icon overrides. See how to override existing icons in the [Backstage documentation](https://backstage.io/docs/getting-started/app-custom-theme/#custom-icons).
26
+ - 836127c: Updated dependency `@testing-library/react` to `^16.0.0`.
27
+ - Updated dependencies
28
+ - @backstage/core-plugin-api@1.9.4-next.0
29
+ - @backstage/version-bridge@1.0.9-next.0
30
+ - @backstage/config@1.2.0
31
+ - @backstage/types@1.1.1
32
+
3
33
  ## 1.14.2
4
34
 
5
35
  ### Patch Changes
@@ -8,7 +8,34 @@ function defaultConfigLoaderSync(runtimeConfigJson = "__APP_INJECTED_RUNTIME_CON
8
8
  throw new Error("Static configuration has invalid format");
9
9
  }
10
10
  const configs = appConfig.slice();
11
- if (runtimeConfigJson !== "__app_injected_runtime_config__".toLocaleUpperCase("en-US")) {
11
+ const configScripts = document.querySelectorAll(
12
+ 'script[type="backstage.io/config"]'
13
+ );
14
+ if (configScripts.length > 0) {
15
+ for (const el of configScripts) {
16
+ try {
17
+ const content = el.textContent;
18
+ if (!content) {
19
+ throw new Error("tag is empty");
20
+ }
21
+ let data;
22
+ try {
23
+ data = JSON.parse(content);
24
+ } catch (error) {
25
+ throw new Error(`failed to parse config; ${error}`);
26
+ }
27
+ if (!Array.isArray(data)) {
28
+ throw new Error("data is not an array");
29
+ }
30
+ configs.push(...data);
31
+ } catch (error) {
32
+ throw new Error(
33
+ `Failed to load config from script tag, ${error.message}`
34
+ );
35
+ }
36
+ }
37
+ } else if (runtimeConfigJson !== // Avoiding this string also being replaced at runtime
38
+ "__app_injected_runtime_config__".toLocaleUpperCase("en-US")) {
12
39
  try {
13
40
  const data = JSON.parse(runtimeConfigJson);
14
41
  if (Array.isArray(data)) {
@@ -1 +1 @@
1
- {"version":3,"file":"defaultConfigLoader.esm.js","sources":["../../src/app/defaultConfigLoader.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { AppConfig } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\nimport { AppConfigLoader } from './types';\n\n/**\n * The default config loader, which expects that config is available at compile-time\n * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as\n * returned by the config loader.\n *\n * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,\n * which can be rewritten at runtime to contain an additional JSON config object.\n * If runtime config is present, it will be placed first in the config array, overriding\n * other config values.\n *\n * @public\n */\nexport const defaultConfigLoader: AppConfigLoader = async () =>\n defaultConfigLoaderSync();\n\n/** @internal */\nexport function defaultConfigLoaderSync(\n // This string may be replaced at runtime to provide additional config.\n // It should be replaced by a JSON-serialized config object.\n // It's a param so we can test it, but at runtime this will always fall back to default.\n runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',\n) {\n const appConfig = process.env.APP_CONFIG;\n if (!appConfig) {\n throw new Error('No static configuration provided');\n }\n if (!Array.isArray(appConfig)) {\n throw new Error('Static configuration has invalid format');\n }\n const configs = appConfig.slice() as unknown as AppConfig[];\n\n // Avoiding this string also being replaced at runtime\n if (\n runtimeConfigJson !==\n '__app_injected_runtime_config__'.toLocaleUpperCase('en-US')\n ) {\n try {\n const data = JSON.parse(runtimeConfigJson) as JsonObject;\n if (Array.isArray(data)) {\n configs.push(...data);\n } else {\n configs.push({ data, context: 'env' });\n }\n } catch (error) {\n throw new Error(`Failed to load runtime configuration, ${error}`);\n }\n }\n\n const windowAppConfig = (window as any).__APP_CONFIG__;\n if (windowAppConfig) {\n configs.push({\n context: 'window',\n data: windowAppConfig,\n });\n }\n return configs;\n}\n"],"names":[],"mappings":"AAgCa,MAAA,mBAAA,GAAuC,YAClD,uBAAwB,GAAA;AAGV,SAAA,uBAAA,CAId,oBAA4B,iCAC5B,EAAA;AACA,EAAM,MAAA,SAAA,GAAY,QAAQ,GAAI,CAAA,UAAA,CAAA;AAC9B,EAAA,IAAI,CAAC,SAAW,EAAA;AACd,IAAM,MAAA,IAAI,MAAM,kCAAkC,CAAA,CAAA;AAAA,GACpD;AACA,EAAA,IAAI,CAAC,KAAA,CAAM,OAAQ,CAAA,SAAS,CAAG,EAAA;AAC7B,IAAM,MAAA,IAAI,MAAM,yCAAyC,CAAA,CAAA;AAAA,GAC3D;AACA,EAAM,MAAA,OAAA,GAAU,UAAU,KAAM,EAAA,CAAA;AAGhC,EAAA,IACE,iBACA,KAAA,iCAAA,CAAkC,iBAAkB,CAAA,OAAO,CAC3D,EAAA;AACA,IAAI,IAAA;AACF,MAAM,MAAA,IAAA,GAAO,IAAK,CAAA,KAAA,CAAM,iBAAiB,CAAA,CAAA;AACzC,MAAI,IAAA,KAAA,CAAM,OAAQ,CAAA,IAAI,CAAG,EAAA;AACvB,QAAQ,OAAA,CAAA,IAAA,CAAK,GAAG,IAAI,CAAA,CAAA;AAAA,OACf,MAAA;AACL,QAAA,OAAA,CAAQ,IAAK,CAAA,EAAE,IAAM,EAAA,OAAA,EAAS,OAAO,CAAA,CAAA;AAAA,OACvC;AAAA,aACO,KAAO,EAAA;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAyC,sCAAA,EAAA,KAAK,CAAE,CAAA,CAAA,CAAA;AAAA,KAClE;AAAA,GACF;AAEA,EAAA,MAAM,kBAAmB,MAAe,CAAA,cAAA,CAAA;AACxC,EAAA,IAAI,eAAiB,EAAA;AACnB,IAAA,OAAA,CAAQ,IAAK,CAAA;AAAA,MACX,OAAS,EAAA,QAAA;AAAA,MACT,IAAM,EAAA,eAAA;AAAA,KACP,CAAA,CAAA;AAAA,GACH;AACA,EAAO,OAAA,OAAA,CAAA;AACT;;;;"}
1
+ {"version":3,"file":"defaultConfigLoader.esm.js","sources":["../../src/app/defaultConfigLoader.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { AppConfig } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\nimport { AppConfigLoader } from './types';\n\n/**\n * The default config loader, which expects that config is available at compile-time\n * in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as\n * returned by the config loader.\n *\n * It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,\n * which can be rewritten at runtime to contain an additional JSON config object.\n * If runtime config is present, it will be placed first in the config array, overriding\n * other config values.\n *\n * @public\n */\nexport const defaultConfigLoader: AppConfigLoader = async () =>\n defaultConfigLoaderSync();\n\n/** @internal */\nexport function defaultConfigLoaderSync(\n // This string may be replaced at runtime to provide additional config.\n // It should be replaced by a JSON-serialized config object.\n // It's a param so we can test it, but at runtime this will always fall back to default.\n runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',\n) {\n const appConfig = process.env.APP_CONFIG;\n if (!appConfig) {\n throw new Error('No static configuration provided');\n }\n if (!Array.isArray(appConfig)) {\n throw new Error('Static configuration has invalid format');\n }\n const configs = appConfig.slice() as unknown as AppConfig[];\n\n // Check if we have any config script tags, otherwise fall back to injected config\n const configScripts = document.querySelectorAll(\n 'script[type=\"backstage.io/config\"]',\n );\n if (configScripts.length > 0) {\n for (const el of configScripts) {\n try {\n const content = el.textContent;\n if (!content) {\n throw new Error('tag is empty');\n }\n let data;\n try {\n data = JSON.parse(content);\n } catch (error) {\n throw new Error(`failed to parse config; ${error}`);\n }\n if (!Array.isArray(data)) {\n throw new Error('data is not an array');\n }\n configs.push(...data);\n } catch (error) {\n throw new Error(\n `Failed to load config from script tag, ${error.message}`,\n );\n }\n }\n } else if (\n runtimeConfigJson !==\n // Avoiding this string also being replaced at runtime\n '__app_injected_runtime_config__'.toLocaleUpperCase('en-US')\n ) {\n try {\n const data = JSON.parse(runtimeConfigJson) as JsonObject;\n if (Array.isArray(data)) {\n configs.push(...data);\n } else {\n configs.push({ data, context: 'env' });\n }\n } catch (error) {\n throw new Error(`Failed to load runtime configuration, ${error}`);\n }\n }\n\n const windowAppConfig = (window as any).__APP_CONFIG__;\n if (windowAppConfig) {\n configs.push({\n context: 'window',\n data: windowAppConfig,\n });\n }\n return configs;\n}\n"],"names":[],"mappings":"AAgCa,MAAA,mBAAA,GAAuC,YAClD,uBAAwB,GAAA;AAGV,SAAA,uBAAA,CAId,oBAA4B,iCAC5B,EAAA;AACA,EAAM,MAAA,SAAA,GAAY,QAAQ,GAAI,CAAA,UAAA,CAAA;AAC9B,EAAA,IAAI,CAAC,SAAW,EAAA;AACd,IAAM,MAAA,IAAI,MAAM,kCAAkC,CAAA,CAAA;AAAA,GACpD;AACA,EAAA,IAAI,CAAC,KAAA,CAAM,OAAQ,CAAA,SAAS,CAAG,EAAA;AAC7B,IAAM,MAAA,IAAI,MAAM,yCAAyC,CAAA,CAAA;AAAA,GAC3D;AACA,EAAM,MAAA,OAAA,GAAU,UAAU,KAAM,EAAA,CAAA;AAGhC,EAAA,MAAM,gBAAgB,QAAS,CAAA,gBAAA;AAAA,IAC7B,oCAAA;AAAA,GACF,CAAA;AACA,EAAI,IAAA,aAAA,CAAc,SAAS,CAAG,EAAA;AAC5B,IAAA,KAAA,MAAW,MAAM,aAAe,EAAA;AAC9B,MAAI,IAAA;AACF,QAAA,MAAM,UAAU,EAAG,CAAA,WAAA,CAAA;AACnB,QAAA,IAAI,CAAC,OAAS,EAAA;AACZ,UAAM,MAAA,IAAI,MAAM,cAAc,CAAA,CAAA;AAAA,SAChC;AACA,QAAI,IAAA,IAAA,CAAA;AACJ,QAAI,IAAA;AACF,UAAO,IAAA,GAAA,IAAA,CAAK,MAAM,OAAO,CAAA,CAAA;AAAA,iBAClB,KAAO,EAAA;AACd,UAAA,MAAM,IAAI,KAAA,CAAM,CAA2B,wBAAA,EAAA,KAAK,CAAE,CAAA,CAAA,CAAA;AAAA,SACpD;AACA,QAAA,IAAI,CAAC,KAAA,CAAM,OAAQ,CAAA,IAAI,CAAG,EAAA;AACxB,UAAM,MAAA,IAAI,MAAM,sBAAsB,CAAA,CAAA;AAAA,SACxC;AACA,QAAQ,OAAA,CAAA,IAAA,CAAK,GAAG,IAAI,CAAA,CAAA;AAAA,eACb,KAAO,EAAA;AACd,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAA,uCAAA,EAA0C,MAAM,OAAO,CAAA,CAAA;AAAA,SACzD,CAAA;AAAA,OACF;AAAA,KACF;AAAA,GAEA,MAAA,IAAA,iBAAA;AAAA,EAEA,iCAAA,CAAkC,iBAAkB,CAAA,OAAO,CAC3D,EAAA;AACA,IAAI,IAAA;AACF,MAAM,MAAA,IAAA,GAAO,IAAK,CAAA,KAAA,CAAM,iBAAiB,CAAA,CAAA;AACzC,MAAI,IAAA,KAAA,CAAM,OAAQ,CAAA,IAAI,CAAG,EAAA;AACvB,QAAQ,OAAA,CAAA,IAAA,CAAK,GAAG,IAAI,CAAA,CAAA;AAAA,OACf,MAAA;AACL,QAAA,OAAA,CAAQ,IAAK,CAAA,EAAE,IAAM,EAAA,OAAA,EAAS,OAAO,CAAA,CAAA;AAAA,OACvC;AAAA,aACO,KAAO,EAAA;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAyC,sCAAA,EAAA,KAAK,CAAE,CAAA,CAAA,CAAA;AAAA,KAClE;AAAA,GACF;AAEA,EAAA,MAAM,kBAAmB,MAAe,CAAA,cAAA,CAAA;AACxC,EAAA,IAAI,eAAiB,EAAA;AACnB,IAAA,OAAA,CAAQ,IAAK,CAAA;AAAA,MACX,OAAS,EAAA,QAAA;AAAA,MACT,IAAM,EAAA,eAAA;AAAA,KACP,CAAA,CAAA;AAAA,GACH;AACA,EAAO,OAAA,OAAA,CAAA;AACT;;;;"}
@@ -22,6 +22,7 @@ function collectRouteIds(plugins) {
22
22
  function resolveRouteBindings(bindRoutes, config, plugins) {
23
23
  const routesById = collectRouteIds(plugins);
24
24
  const result = /* @__PURE__ */ new Map();
25
+ const disabledExternalRefs = /* @__PURE__ */ new Set();
25
26
  if (bindRoutes) {
26
27
  const bind = (externalRoutes, targetRoutes) => {
27
28
  for (const [key, value] of Object.entries(targetRoutes)) {
@@ -31,18 +32,19 @@ function resolveRouteBindings(bindRoutes, config, plugins) {
31
32
  }
32
33
  if (!value && !externalRoute.optional) {
33
34
  throw new Error(
34
- `External route ${key} is required but was undefined`
35
+ `External route ${key} is required but was ${value === false ? "disabled" : "not provided"}`
35
36
  );
36
37
  }
37
38
  if (value) {
38
39
  result.set(externalRoute, value);
40
+ } else if (value === false) {
41
+ disabledExternalRefs.add(externalRoute);
39
42
  }
40
43
  }
41
44
  };
42
45
  bindRoutes({ bind });
43
46
  }
44
47
  const bindings = config.getOptionalConfig("app.routes.bindings")?.get();
45
- const disabledExternalRefs = /* @__PURE__ */ new Set();
46
48
  if (bindings) {
47
49
  for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
48
50
  if (!isValidTargetRefId(targetRefId)) {
@@ -56,10 +58,12 @@ function resolveRouteBindings(bindRoutes, config, plugins) {
56
58
  `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`
57
59
  );
58
60
  }
61
+ if (result.has(externalRef) || disabledExternalRefs.has(externalRef)) {
62
+ continue;
63
+ }
59
64
  if (targetRefId === false) {
60
65
  disabledExternalRefs.add(externalRef);
61
- result.delete(externalRef);
62
- } else if (!result.has(externalRef)) {
66
+ } else {
63
67
  const targetRef = routesById.routes.get(targetRefId);
64
68
  if (!targetRef) {
65
69
  throw new Error(
@@ -1 +1 @@
1
- {"version":3,"file":"resolveRouteBindings.esm.js","sources":["../../src/app/resolveRouteBindings.ts"],"sourcesContent":["/*\n * Copyright 2020 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 {\n RouteRef,\n SubRouteRef,\n ExternalRouteRef,\n BackstagePlugin,\n AnyRoutes,\n AnyExternalRoutes,\n} from '@backstage/core-plugin-api';\nimport { AppOptions, AppRouteBinder } from './types';\nimport { Config } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\n\n/** @internal */\nexport function collectRouteIds(\n plugins: Iterable<\n Pick<\n BackstagePlugin<AnyRoutes, AnyExternalRoutes>,\n 'getId' | 'routes' | 'externalRoutes'\n >\n >,\n) {\n const routesById = new Map<string, RouteRef | SubRouteRef>();\n const externalRoutesById = new Map<string, ExternalRouteRef>();\n\n for (const plugin of plugins) {\n for (const [name, ref] of Object.entries(plugin.routes ?? {})) {\n const refId = `${plugin.getId()}.${name}`;\n if (routesById.has(refId)) {\n throw new Error(`Unexpected duplicate route '${refId}'`);\n }\n\n routesById.set(refId, ref);\n }\n for (const [name, ref] of Object.entries(plugin.externalRoutes ?? {})) {\n const refId = `${plugin.getId()}.${name}`;\n if (externalRoutesById.has(refId)) {\n throw new Error(`Unexpected duplicate external route '${refId}'`);\n }\n\n externalRoutesById.set(refId, ref);\n }\n }\n\n return { routes: routesById, externalRoutes: externalRoutesById };\n}\n\n/** @internal */\nexport function resolveRouteBindings(\n bindRoutes: AppOptions['bindRoutes'],\n config: Config,\n plugins: Iterable<\n Pick<\n BackstagePlugin<AnyRoutes, AnyExternalRoutes>,\n 'getId' | 'routes' | 'externalRoutes'\n >\n >,\n) {\n const routesById = collectRouteIds(plugins);\n const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();\n\n // Perform callback bindings first with highest priority\n if (bindRoutes) {\n const bind: AppRouteBinder = (\n externalRoutes,\n targetRoutes: { [name: string]: RouteRef | SubRouteRef },\n ) => {\n for (const [key, value] of Object.entries(targetRoutes)) {\n const externalRoute = externalRoutes[key];\n if (!externalRoute) {\n throw new Error(`Key ${key} is not an existing external route`);\n }\n if (!value && !externalRoute.optional) {\n throw new Error(\n `External route ${key} is required but was undefined`,\n );\n }\n if (value) {\n result.set(externalRoute, value);\n }\n }\n };\n bindRoutes({ bind });\n }\n\n // Then perform config based bindings with lower priority\n const bindings = config\n .getOptionalConfig('app.routes.bindings')\n ?.get<JsonObject>();\n const disabledExternalRefs = new Set<ExternalRouteRef>();\n if (bindings) {\n for (const [externalRefId, targetRefId] of Object.entries(bindings)) {\n if (!isValidTargetRefId(targetRefId)) {\n throw new Error(\n `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`,\n );\n }\n\n const externalRef = routesById.externalRoutes.get(externalRefId);\n if (!externalRef) {\n throw new Error(\n `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,\n );\n }\n\n if (targetRefId === false) {\n disabledExternalRefs.add(externalRef);\n\n result.delete(externalRef);\n } else if (!result.has(externalRef)) {\n const targetRef = routesById.routes.get(targetRefId);\n if (!targetRef) {\n throw new Error(\n `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,\n );\n }\n\n result.set(externalRef, targetRef);\n }\n }\n }\n\n // Finally fall back to attempting to map defaults, at lowest priority\n for (const externalRef of routesById.externalRoutes.values()) {\n if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) {\n const defaultRefId =\n 'getDefaultTarget' in externalRef\n ? (externalRef.getDefaultTarget as () => string | undefined)()\n : undefined;\n if (defaultRefId) {\n const defaultRef = routesById.routes.get(defaultRefId);\n if (defaultRef) {\n result.set(externalRef, defaultRef);\n }\n }\n }\n }\n\n return result;\n}\n\nfunction isValidTargetRefId(value: unknown): value is string | false {\n if (value === false) {\n return true;\n }\n\n if (typeof value === 'string' && value) {\n return true;\n }\n\n return false;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resolveRouteBindings.esm.js","sources":["../../src/app/resolveRouteBindings.ts"],"sourcesContent":["/*\n * Copyright 2020 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 {\n RouteRef,\n SubRouteRef,\n ExternalRouteRef,\n BackstagePlugin,\n AnyRoutes,\n AnyExternalRoutes,\n} from '@backstage/core-plugin-api';\nimport { AppOptions, AppRouteBinder } from './types';\nimport { Config } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\n\n/** @internal */\nexport function collectRouteIds(\n plugins: Iterable<\n Pick<\n BackstagePlugin<AnyRoutes, AnyExternalRoutes>,\n 'getId' | 'routes' | 'externalRoutes'\n >\n >,\n) {\n const routesById = new Map<string, RouteRef | SubRouteRef>();\n const externalRoutesById = new Map<string, ExternalRouteRef>();\n\n for (const plugin of plugins) {\n for (const [name, ref] of Object.entries(plugin.routes ?? {})) {\n const refId = `${plugin.getId()}.${name}`;\n if (routesById.has(refId)) {\n throw new Error(`Unexpected duplicate route '${refId}'`);\n }\n\n routesById.set(refId, ref);\n }\n for (const [name, ref] of Object.entries(plugin.externalRoutes ?? {})) {\n const refId = `${plugin.getId()}.${name}`;\n if (externalRoutesById.has(refId)) {\n throw new Error(`Unexpected duplicate external route '${refId}'`);\n }\n\n externalRoutesById.set(refId, ref);\n }\n }\n\n return { routes: routesById, externalRoutes: externalRoutesById };\n}\n\n/** @internal */\nexport function resolveRouteBindings(\n bindRoutes: AppOptions['bindRoutes'],\n config: Config,\n plugins: Iterable<\n Pick<\n BackstagePlugin<AnyRoutes, AnyExternalRoutes>,\n 'getId' | 'routes' | 'externalRoutes'\n >\n >,\n) {\n const routesById = collectRouteIds(plugins);\n const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();\n const disabledExternalRefs = new Set<ExternalRouteRef>();\n\n // Perform callback bindings first with highest priority\n if (bindRoutes) {\n const bind: AppRouteBinder = (\n externalRoutes,\n targetRoutes: { [name: string]: RouteRef | SubRouteRef },\n ) => {\n for (const [key, value] of Object.entries(targetRoutes)) {\n const externalRoute = externalRoutes[key];\n if (!externalRoute) {\n throw new Error(`Key ${key} is not an existing external route`);\n }\n if (!value && !externalRoute.optional) {\n throw new Error(\n `External route ${key} is required but was ${\n value === false ? 'disabled' : 'not provided'\n }`,\n );\n }\n if (value) {\n result.set(externalRoute, value);\n } else if (value === false) {\n disabledExternalRefs.add(externalRoute);\n }\n }\n };\n bindRoutes({ bind });\n }\n\n // Then perform config based bindings with lower priority\n const bindings = config\n .getOptionalConfig('app.routes.bindings')\n ?.get<JsonObject>();\n if (bindings) {\n for (const [externalRefId, targetRefId] of Object.entries(bindings)) {\n if (!isValidTargetRefId(targetRefId)) {\n throw new Error(\n `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`,\n );\n }\n\n const externalRef = routesById.externalRoutes.get(externalRefId);\n if (!externalRef) {\n throw new Error(\n `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,\n );\n }\n\n // Skip if binding was already defined in code\n if (result.has(externalRef) || disabledExternalRefs.has(externalRef)) {\n continue;\n }\n\n if (targetRefId === false) {\n disabledExternalRefs.add(externalRef);\n } else {\n const targetRef = routesById.routes.get(targetRefId);\n if (!targetRef) {\n throw new Error(\n `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,\n );\n }\n\n result.set(externalRef, targetRef);\n }\n }\n }\n\n // Finally fall back to attempting to map defaults, at lowest priority\n for (const externalRef of routesById.externalRoutes.values()) {\n if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) {\n const defaultRefId =\n 'getDefaultTarget' in externalRef\n ? (externalRef.getDefaultTarget as () => string | undefined)()\n : undefined;\n if (defaultRefId) {\n const defaultRef = routesById.routes.get(defaultRefId);\n if (defaultRef) {\n result.set(externalRef, defaultRef);\n }\n }\n }\n }\n\n return result;\n}\n\nfunction isValidTargetRefId(value: unknown): value is string | false {\n if (value === false) {\n return true;\n }\n\n if (typeof value === 'string' && value) {\n return true;\n }\n\n return false;\n}\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -766,6 +766,8 @@ type AppIcons = {
766
766
  techdocs: IconComponent;
767
767
  user: IconComponent;
768
768
  warning: IconComponent;
769
+ star: IconComponent;
770
+ unstarred: IconComponent;
769
771
  };
770
772
  /**
771
773
  * A function that loads in the App config that will be accessible via the ConfigApi.
@@ -802,7 +804,7 @@ type PartialKeys<Map extends {
802
804
  type TargetRouteMap<ExternalRoutes extends {
803
805
  [name: string]: ExternalRouteRef;
804
806
  }> = {
805
- [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<infer Params, any> ? RouteRef<Params> | SubRouteRef<Params> : never;
807
+ [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<infer Params, any> ? RouteRef<Params> | SubRouteRef<Params> | false : never;
806
808
  };
807
809
  /**
808
810
  * A function that can bind from external routes of a given plugin, to concrete
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/core-app-api",
3
3
  "description": "Core app API used by Backstage apps",
4
- "version": "1.14.2",
4
+ "version": "1.15.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -40,9 +40,9 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@backstage/config": "^1.2.0",
43
- "@backstage/core-plugin-api": "^1.9.3",
43
+ "@backstage/core-plugin-api": "^1.9.4",
44
44
  "@backstage/types": "^1.1.1",
45
- "@backstage/version-bridge": "^1.0.8",
45
+ "@backstage/version-bridge": "^1.0.9",
46
46
  "@types/prop-types": "^15.7.3",
47
47
  "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
48
48
  "history": "^5.0.0",
@@ -59,11 +59,11 @@
59
59
  "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@backstage/cli": "^0.27.0",
63
- "@backstage/test-utils": "^1.5.10",
62
+ "@backstage/cli": "^0.27.1",
63
+ "@backstage/test-utils": "^1.6.0",
64
64
  "@testing-library/dom": "^10.0.0",
65
65
  "@testing-library/jest-dom": "^6.0.0",
66
- "@testing-library/react": "^15.0.0",
66
+ "@testing-library/react": "^16.0.0",
67
67
  "@testing-library/react-hooks": "^8.0.0",
68
68
  "@testing-library/user-event": "^14.0.0",
69
69
  "@types/zen-observable": "^0.8.0",