Merge pull request #13927 from sennyeya/NewCliOptionsPublicPathAndBackendUrl
Update AppManager to Allow for Multiple Stages with a Single Build
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Fix webpack dev server issue where it wasn't serving `index.html` from correct endpoint on subsequent requests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Apps will now rewrite the `app.baseUrl` configuration to match the current `location.origin`. The `backend.baseUrl` will also be rewritten in the same way when the `app.baseUrl` and `backend.baseUrl` have matching origins. This will reduce the need for separate frontend builds for different environments.
|
||||
@@ -60,6 +60,9 @@ export async function serveBundle(options: ServeOptions) {
|
||||
// Paths with dots should still use the history fallback.
|
||||
// See https://github.com/facebookincubator/create-react-app/issues/387.
|
||||
disableDotRule: true,
|
||||
|
||||
// The index needs to be rewritten relative to the new public path, including subroutes.
|
||||
index: `${config.output?.publicPath}index.html`,
|
||||
},
|
||||
https:
|
||||
url.protocol === 'https:'
|
||||
|
||||
@@ -81,7 +81,9 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => {
|
||||
configLoader: async () => [
|
||||
{
|
||||
context: 'test',
|
||||
data: { app: { baseUrl: 'http://localhost/foo' } },
|
||||
data: {
|
||||
app: { baseUrl: 'http://localhost/foo' },
|
||||
},
|
||||
},
|
||||
],
|
||||
bindRoutes: () => {},
|
||||
|
||||
@@ -62,7 +62,9 @@ describe('AppManager', () => {
|
||||
configLoader: async () => [
|
||||
{
|
||||
context: 'test',
|
||||
data: { app: { baseUrl: 'http://localhost/foo' } },
|
||||
data: {
|
||||
app: { baseUrl: 'http://localhost/foo' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -94,7 +96,9 @@ describe('AppManager', () => {
|
||||
configLoader: async () => [
|
||||
{
|
||||
context: 'test',
|
||||
data: { app: { baseUrl: 'http://localhost/foo' } },
|
||||
data: {
|
||||
app: { baseUrl: 'http://localhost/foo' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -620,4 +620,110 @@ describe('Integration Test', () => {
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('relative url resolvers', () => {
|
||||
it.each([
|
||||
[
|
||||
[document.location.href, document.location.href],
|
||||
{
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:8008/',
|
||||
},
|
||||
app: {
|
||||
baseUrl: 'http://localhost:8008/',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
`${document.location.origin}/backstage`,
|
||||
`${document.location.origin}/backstage`,
|
||||
],
|
||||
{
|
||||
backend: {
|
||||
baseUrl: 'http://test.com/backstage',
|
||||
},
|
||||
app: {
|
||||
baseUrl: 'http://test.com/backstage',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
`${document.location.origin}/backstage/instance`,
|
||||
`${document.location.origin}/backstage/instance`,
|
||||
],
|
||||
{
|
||||
backend: {
|
||||
baseUrl: 'http://test.com/backstage/instance',
|
||||
},
|
||||
app: {
|
||||
baseUrl: 'http://test.com/backstage/instance',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
`${document.location.origin}/backstage/instance`,
|
||||
`http://test.com/backstage/instance`,
|
||||
],
|
||||
{
|
||||
backend: {
|
||||
baseUrl: 'http://test.com/backstage/instance',
|
||||
},
|
||||
app: {
|
||||
baseUrl: 'http://test-front.com/backstage/instance',
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should be %p when %p',
|
||||
async ([expectedAppUrl, expectedBackendUrl], data) => {
|
||||
const app = new AppManager({
|
||||
apis: [],
|
||||
defaultApis: [],
|
||||
themes: [
|
||||
{
|
||||
id: 'light',
|
||||
title: 'Light Theme',
|
||||
variant: 'light',
|
||||
Provider: ({ children }) => <>{children}</>,
|
||||
},
|
||||
],
|
||||
icons,
|
||||
plugins: [],
|
||||
components,
|
||||
configLoader: async () => [
|
||||
{
|
||||
context: 'test',
|
||||
data,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const Provider = app.getProvider();
|
||||
const ConfigDisplay = ({ configString }: { configString: string }) => {
|
||||
const config = useApi(configApiRef);
|
||||
return (
|
||||
<span>
|
||||
{configString}: {config?.getString(configString)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const dom = await renderWithEffects(
|
||||
<Provider>
|
||||
<ConfigDisplay configString="app.baseUrl" />
|
||||
<ConfigDisplay configString="backend.baseUrl" />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(dom.getByText(`app.baseUrl: ${expectedAppUrl}`)).toBeTruthy();
|
||||
|
||||
expect(
|
||||
dom.getByText(`backend.baseUrl: ${expectedBackendUrl}`),
|
||||
).toBeTruthy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { AppConfig, Config } from '@backstage/config';
|
||||
import React, {
|
||||
ComponentType,
|
||||
createContext,
|
||||
@@ -154,7 +154,71 @@ function useConfigLoader(
|
||||
};
|
||||
}
|
||||
|
||||
const configReader = ConfigReader.fromConfigs(config.value ?? []);
|
||||
let configReader;
|
||||
/**
|
||||
* config.value can be undefined or empty. If it's either, don't bother overriding anything.
|
||||
*/
|
||||
if (config.value?.length) {
|
||||
const urlConfigReader = ConfigReader.fromConfigs(config.value);
|
||||
|
||||
/**
|
||||
* Return the origin of the given URL.
|
||||
* @param url An absolute URL.
|
||||
* @returns The given URL's origin.
|
||||
* @throws If fullUrl is not a correctly formatted absolute URL.
|
||||
*/
|
||||
const getOrigin = (url: string) => new URL(url).origin;
|
||||
|
||||
/**
|
||||
* Resolve an absolute URL as relative to the current document.
|
||||
* @param fullUrl URL to resolve.
|
||||
* @returns Absolute URL with origin as the current document origin.
|
||||
* @throws If fullUrl is not a correctly formatted absolute URL.
|
||||
*/
|
||||
const overrideOrigin = (fullUrl: string) => {
|
||||
return new URL(
|
||||
fullUrl.replace(getOrigin(fullUrl), ''),
|
||||
document.location.origin,
|
||||
).href;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test configs may not define `app.baseUrl` or `backend.baseUrl` and we
|
||||
* don't want to enforce here.
|
||||
*/
|
||||
const appBaseUrl = urlConfigReader.getOptionalString('app.baseUrl');
|
||||
const backendBaseUrl = urlConfigReader.getOptionalString('backend.baseUrl');
|
||||
|
||||
let configs = config.value;
|
||||
const relativeResolverConfig: AppConfig = {
|
||||
data: {},
|
||||
context: 'relative-resolver',
|
||||
};
|
||||
if (appBaseUrl && backendBaseUrl) {
|
||||
const appOrigin = getOrigin(appBaseUrl);
|
||||
const backendOrigin = getOrigin(backendBaseUrl);
|
||||
|
||||
if (appOrigin === backendOrigin) {
|
||||
relativeResolverConfig.data.backend = {
|
||||
baseUrl: overrideOrigin(backendBaseUrl),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (appBaseUrl) {
|
||||
relativeResolverConfig.data.app = {
|
||||
baseUrl: overrideOrigin(appBaseUrl),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Only add the relative config if there is actually data to add.
|
||||
*/
|
||||
if (Object.keys(relativeResolverConfig.data).length) {
|
||||
configs = configs.concat([relativeResolverConfig]);
|
||||
}
|
||||
configReader = ConfigReader.fromConfigs(configs);
|
||||
} else {
|
||||
configReader = ConfigReader.fromConfigs([]);
|
||||
}
|
||||
|
||||
return { api: configReader };
|
||||
}
|
||||
|
||||
@@ -24,7 +24,12 @@ const anyEnv = (process.env = { ...process.env }) as any;
|
||||
describe('DevAppBuilder', () => {
|
||||
it('should be able to render a component in a dev app', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ context: 'test', data: { app: { title: 'Test App' } } },
|
||||
{
|
||||
context: 'test',
|
||||
data: {
|
||||
app: { title: 'Test App' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const MyComponent = () => {
|
||||
|
||||
Reference in New Issue
Block a user