diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 3f5cfc1c58..c53ce63ada 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -64,10 +64,19 @@ export const apis = [ createApiFactory({ api: discoveryApiRef, deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/api/{{ pluginId }}`, - ), + factory: ({ configApi }) => { + let baseUrl; + try { + // Try parsing the url relative to the current document origin, if it fails the URL is misformed. + baseUrl = new URL( + configApi.getString('backend.baseUrl'), + document.location.origin, + ).href.replace(/\/$/, ''); + } catch (err) { + baseUrl = configApi.getString('backend.baseUrl'); + } + return UrlPatternDiscovery.compile(`${baseUrl}/api/{{ pluginId }}`); + }, }), createApiFactory({ api: alertApiRef, diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 6d7ca2e6a9..7f1687ca03 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -19,15 +19,17 @@ import { resolve as resolvePath } from 'path'; import { buildBundle } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; +import { CliConfigOptions } from '@backstage/config-loader/src/lib/cli'; interface BuildAppOptions { targetDir: string; writeStats: boolean; + cliOptions?: CliConfigOptions; configPaths: string[]; } export async function buildFrontend(options: BuildAppOptions) { - const { targetDir, writeStats, configPaths } = options; + const { targetDir, writeStats, configPaths, cliOptions } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); await buildBundle({ targetDir, @@ -37,6 +39,7 @@ export async function buildFrontend(options: BuildAppOptions) { ...(await loadCliConfig({ args: configPaths, fromPackage: name, + cliOptions, })), }); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 82cb60e62f..33291cb70c 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -39,6 +39,14 @@ export function registerRepoCommand(program: Command) { '--all', 'Build all packages, including bundled app and backend packages.', ) + .option( + '--public-path ', + 'Public path for hosting the website on, can be relative.', + ) + .option( + '--backend-url ', + 'Backend url, expects just the origin or sub-route. Do not include /api. Can be relative.', + ) .option( '--since ', 'Only build packages and their dev dependents that changed since the specified ref', diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index 21b1580cb9..86d2e781a6 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -153,6 +153,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { return; } await buildFrontend({ + cliOptions: { + publicPath: opts.publicPath, + backendUrl: opts.backendUrl, + }, targetDir: pkg.dir, configPaths: (buildOptions.config as string[]) ?? [], writeStats: Boolean(buildOptions.stats), diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 1974153f02..bc0ff2db3b 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -36,10 +36,12 @@ import { runPlain } from '../run'; import ESLintPlugin from 'eslint-webpack-plugin'; import pickBy from 'lodash/pickBy'; +const DUMMY_URL = 'http://dummyurl.org'; + export function resolveBaseUrl(config: Config): URL { const baseUrl = config.getString('app.baseUrl'); try { - return new URL(baseUrl); + return new URL(baseUrl, DUMMY_URL); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } @@ -88,7 +90,7 @@ export async function createConfig( const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir)); const baseUrl = frontendConfig.getString('app.baseUrl'); - const validBaseUrl = new URL(baseUrl); + const validBaseUrl = new URL(baseUrl, DUMMY_URL); const publicPath = validBaseUrl.pathname.replace(/\/$/, ''); if (checksEnabled) { plugins.push( diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index fb11316fa6..1e76fbc662 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/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index a76571c3f6..e28f2d7f6f 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -32,6 +32,10 @@ type Options = { withFilteredKeys?: boolean; withDeprecatedKeys?: boolean; fullVisibility?: boolean; + cliOptions?: { + publicPath?: string; + backendUrl?: string; + }; }; export async function loadCliConfig(options: Options) { @@ -76,6 +80,7 @@ export async function loadCliConfig(options: Options) { experimentalEnvFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, + cliOptions: options.cliOptions, configRoot: paths.targetRoot, configTargets: configTargets, }); diff --git a/packages/config-loader/src/lib/cli.ts b/packages/config-loader/src/lib/cli.ts new file mode 100644 index 0000000000..9d935e2826 --- /dev/null +++ b/packages/config-loader/src/lib/cli.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; + +export type CliConfigOptions = { + publicPath?: string; + backendUrl?: string; +}; + +/** + * Read specific parameters from the CLI and add them to the build config. + * @param opts CLI passed parameters. + * @returns Array of config, empty if there is no relevant passed in cli options. + * + * @public + */ +export function readCliConfig(opts?: CliConfigOptions): AppConfig[] { + if (!opts || Object.keys(opts).length === 0) return []; + const data: JsonObject = {}; + + if (opts.publicPath) { + data.app = { + baseUrl: opts.publicPath, + }; + } + + if (opts.backendUrl) { + data.backend = { + baseUrl: opts.backendUrl, + }; + } + + return [{ data, context: 'cli' }]; +} diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index f5ce0eb80c..44bd4f94e8 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -28,6 +28,7 @@ import { readEnvConfig, } from './lib'; import fetch from 'node-fetch'; +import { CliConfigOptions, readCliConfig } from './lib/cli'; /** @public */ export type ConfigTarget = { path: string } | { url: string }; @@ -81,6 +82,11 @@ export type LoadConfigOptions = { * An optional configuration that enables watching of config files. */ watch?: LoadConfigOptionsWatch; + + /** + * New options from the CLI that affect the build config. + */ + cliOptions?: CliConfigOptions; }; /** @@ -230,6 +236,8 @@ export async function loadConfig( } } + const cliConfigs = readCliConfig(options.cliOptions); + const envConfigs = readEnvConfig(process.env); const watchConfigFile = (watchProp: LoadConfigOptionsWatch) => { @@ -318,7 +326,7 @@ export async function loadConfig( return { appConfigs: remote - ? [...remoteConfigs, ...fileConfigs, ...envConfigs] - : [...fileConfigs, ...envConfigs], + ? [...remoteConfigs, ...fileConfigs, ...envConfigs, ...cliConfigs] + : [...fileConfigs, ...envConfigs, ...cliConfigs], }; } diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 0ca09c0a0c..9b3dc841d0 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -116,7 +116,7 @@ function getBasePath(configApi: Config) { function readBasePath(configApi: ConfigApi) { let { pathname } = new URL( configApi.getOptionalString('app.baseUrl') ?? '/', - 'http://dummy.dev', // baseUrl can be specified as just a path + document.location.origin, // baseUrl can be specified as just a path ); pathname = pathname.replace(/\/*$/, ''); return pathname; @@ -332,7 +332,6 @@ export class AppManager implements BackstageApp { routeParents={routing.parents} routeObjects={routing.objects} routeBindings={routeBindings} - basePath={getBasePath(loadedConfig.api)} > + diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 70022534a1..b548533df3 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -60,6 +60,10 @@ export type ErrorBoundaryFallbackProps = { resetError: () => void; }; +export type RouterProps = { + basename: string; +}; + /** * A set of replaceable core components that are part of every Backstage app. * diff --git a/packages/core-app-api/src/routing/RouteResolver.ts b/packages/core-app-api/src/routing/RouteResolver.ts index f17c549bc6..1b23aa30f1 100644 --- a/packages/core-app-api/src/routing/RouteResolver.ts +++ b/packages/core-app-api/src/routing/RouteResolver.ts @@ -214,15 +214,13 @@ export class RouteResolver { // Next we figure out the base path, which is the combination of the common parent path // between our current location and our target location, as well as the additional path // that is the difference between the parent path and the base of our target location. - const basePath = - this.appBasePath + - resolveBasePath( - targetRef, - relativeSourceLocation, - this.routePaths, - this.routeParents, - this.routeObjects, - ); + const basePath = resolveBasePath( + targetRef, + relativeSourceLocation, + this.routePaths, + this.routeParents, + this.routeObjects, + ); const routeFunc: RouteFunc = (...[params]) => { return joinPaths(basePath, generatePath(targetPath, params));