diff --git a/.changeset/five-trainers-admire.md b/.changeset/five-trainers-admire.md new file mode 100644 index 0000000000..e624fc222a --- /dev/null +++ b/.changeset/five-trainers-admire.md @@ -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. diff --git a/.changeset/young-chairs-check.md b/.changeset/young-chairs-check.md new file mode 100644 index 0000000000..e70073e9df --- /dev/null +++ b/.changeset/young-chairs-check.md @@ -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. diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index fb11316fa6..cef55d3265 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -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:' diff --git a/packages/core-app-api/src/app/AppManager.compat.test.tsx b/packages/core-app-api/src/app/AppManager.compat.test.tsx index bdebd79dc4..cfbcc87a01 100644 --- a/packages/core-app-api/src/app/AppManager.compat.test.tsx +++ b/packages/core-app-api/src/app/AppManager.compat.test.tsx @@ -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: () => {}, diff --git a/packages/core-app-api/src/app/AppManager.stable.test.tsx b/packages/core-app-api/src/app/AppManager.stable.test.tsx index a9948a6070..ba16353d47 100644 --- a/packages/core-app-api/src/app/AppManager.stable.test.tsx +++ b/packages/core-app-api/src/app/AppManager.stable.test.tsx @@ -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' }, + }, }, ], }); diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index f81f382650..03bd34eba1 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -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 ( + + {configString}: {config?.getString(configString)} + + ); + }; + + const dom = await renderWithEffects( + + + + , + ); + + expect(dom.getByText(`app.baseUrl: ${expectedAppUrl}`)).toBeTruthy(); + + expect( + dom.getByText(`backend.baseUrl: ${expectedBackendUrl}`), + ).toBeTruthy(); + }, + ); + }); }); diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index b7eafb0100..155ff6ad3b 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -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 }; } diff --git a/packages/dev-utils/src/devApp/render.test.tsx b/packages/dev-utils/src/devApp/render.test.tsx index 83a24bd9b2..2da6ef370a 100644 --- a/packages/dev-utils/src/devApp/render.test.tsx +++ b/packages/dev-utils/src/devApp/render.test.tsx @@ -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 = () => {