diff --git a/.changeset/calm-toes-melt.md b/.changeset/calm-toes-melt.md
new file mode 100644
index 0000000000..b8a7b12c46
--- /dev/null
+++ b/.changeset/calm-toes-melt.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+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.
diff --git a/packages/core-app-api/src/app/defaultConfigLoader.test.ts b/packages/core-app-api/src/app/defaultConfigLoader.test.tsx
similarity index 67%
rename from packages/core-app-api/src/app/defaultConfigLoader.test.ts
rename to packages/core-app-api/src/app/defaultConfigLoader.test.tsx
index c70ec3fbff..44d93aa625 100644
--- a/packages/core-app-api/src/app/defaultConfigLoader.test.ts
+++ b/packages/core-app-api/src/app/defaultConfigLoader.test.tsx
@@ -14,7 +14,10 @@
* limitations under the License.
*/
+import React from 'react';
+import { render } from '@testing-library/react';
import { defaultConfigLoaderSync } from './defaultConfigLoader';
+import { ConfigReader } from '@backstage/config';
(process as any).env = { NODE_ENV: 'test' };
const anyEnv = process.env as any;
@@ -53,6 +56,45 @@ describe('defaultConfigLoaderSync', () => {
]);
});
+ it('loads config from script tags and ignore static config', () => {
+ anyEnv.APP_CONFIG = [];
+
+ render(
+ ,
+ );
+
+ const configs = (defaultConfigLoaderSync as any)('{"my":"runtime-config"}');
+ expect(configs).toEqual([
+ { data: { my: 'config' }, context: 'a' },
+ { data: { my: 'override-config' }, context: 'b' },
+ ]);
+ expect(ConfigReader.fromConfigs(configs).get('my')).toBe('override-config');
+ });
+
+ it('loads config from all script tags in order', () => {
+ anyEnv.APP_CONFIG = [];
+
+ render(
+ <>
+
+
+ >,
+ );
+
+ const configs = (defaultConfigLoaderSync as any)('{"my":"runtime-config"}');
+ expect(configs).toEqual([
+ { data: { my: 'config' }, context: 'a' },
+ { data: { my: 'override-config' }, context: 'b' },
+ ]);
+ expect(ConfigReader.fromConfigs(configs).get('my')).toBe('override-config');
+ });
+
it('fails to load invalid missing config', () => {
expect(() => defaultConfigLoaderSync()).toThrow(
'No static configuration provided',
diff --git a/packages/core-app-api/src/app/defaultConfigLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts
index 301b64a865..bfcef390ec 100644
--- a/packages/core-app-api/src/app/defaultConfigLoader.ts
+++ b/packages/core-app-api/src/app/defaultConfigLoader.ts
@@ -49,9 +49,36 @@ export function defaultConfigLoaderSync(
}
const configs = appConfig.slice() as unknown as AppConfig[];
- // Avoiding this string also being replaced at runtime
- if (
+ // Check if we have any config script tags, otherwise fall back to injected config
+ const configScripts = document.querySelectorAll(
+ 'script[type="backstage.io/config"]',
+ );
+ if (configScripts.length > 0) {
+ for (const el of configScripts) {
+ try {
+ const content = el.textContent;
+ if (!content) {
+ throw new Error('tag is empty');
+ }
+ let data;
+ try {
+ data = JSON.parse(content);
+ } catch (error) {
+ throw new Error(`failed to parse config; ${error}`);
+ }
+ if (!Array.isArray(data)) {
+ throw new Error('data is not an array');
+ }
+ configs.push(...data);
+ } catch (error) {
+ throw new Error(
+ `Failed to load config from script tag, ${error.message}`,
+ );
+ }
+ }
+ } else if (
runtimeConfigJson !==
+ // Avoiding this string also being replaced at runtime
'__app_injected_runtime_config__'.toLocaleUpperCase('en-US')
) {
try {