diff --git a/.changeset/large-tables-wonder.md b/.changeset/large-tables-wonder.md
new file mode 100644
index 0000000000..f54402dbd4
--- /dev/null
+++ b/.changeset/large-tables-wonder.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Add experimental support for optional `auth` app entry point.
diff --git a/packages/app/src/index-public-experimental.tsx b/packages/app/src/index-public-experimental.tsx
new file mode 100644
index 0000000000..9e5f89b4c2
--- /dev/null
+++ b/packages/app/src/index-public-experimental.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 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 { createApp } from '@backstage/app-defaults';
+import { AppRouter } from '@backstage/core-app-api';
+import {
+ AlertDisplay,
+ OAuthRequestDialog,
+ SignInPage,
+} from '@backstage/core-components';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { providers } from '../src/identityProviders';
+import {
+ configApiRef,
+ createApiFactory,
+ discoveryApiRef,
+ useApi,
+} from '@backstage/core-plugin-api';
+import { AuthProxyDiscoveryApi } from '../src/AuthProxyDiscoveryApi';
+
+// TODO(Rugvip): make this available via some util, or maybe Utility API?
+function readBasePath(configApi: typeof configApiRef.T) {
+ let { pathname } = new URL(
+ configApi.getOptionalString('app.baseUrl') ?? '/',
+ 'http://sample.dev', // baseUrl can be specified as just a path
+ );
+ pathname = pathname.replace(/\/*$/, '');
+ return pathname;
+}
+
+const app = createApp({
+ apis: [
+ createApiFactory({
+ api: discoveryApiRef,
+ deps: { configApi: configApiRef },
+ factory: ({ configApi }) => AuthProxyDiscoveryApi.fromConfig(configApi),
+ }),
+ ],
+ components: {
+ SignInPage: props => {
+ return (
+
+ );
+ },
+ },
+});
+
+function RedirectToRoot() {
+ window.location.pathname = readBasePath(useApi(configApiRef));
+ return
;
+}
+
+const App = app.createRoot(
+ <>
+
+
+
+
+
+ >,
+);
+
+ReactDOM.createRoot(document.getElementById('root')!).render();
diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts
index 11e18320f1..d0624c93ff 100644
--- a/packages/cli/src/lib/bundler/bundle.ts
+++ b/packages/cli/src/lib/bundler/bundle.ts
@@ -25,7 +25,7 @@ import {
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import { createConfig, resolveBaseUrl } from './config';
import { BuildOptions } from './types';
-import { resolveBundlingPaths } from './paths';
+import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
import chalk from 'chalk';
import { createDetectedModulesEntryPoint } from './packageDetection';
@@ -47,18 +47,44 @@ export async function buildBundle(options: BuildOptions) {
targetPath: paths.targetPath,
});
- const config = await createConfig(paths, {
+ const commonConfigOptions = {
...options,
checksEnabled: false,
isDev: false,
baseUrl: resolveBaseUrl(options.frontendConfig),
getFrontendAppConfigs: () => options.frontendAppConfigs,
- additionalEntryPoints: detectedModulesEntryPoint,
+ };
+ const configs = [
+ await createConfig(paths, {
+ ...commonConfigOptions,
+ additionalEntryPoints: detectedModulesEntryPoint,
+ }),
+ ];
+
+ const publicPaths = await resolveOptionalBundlingPaths({
+ entry: 'src/index-public-experimental',
+ dist: 'dist/public',
});
+ if (publicPaths) {
+ console.log(
+ chalk.yellow(
+ `⚠️ WARNING: The app /auth entry point is an experimental feature that may receive immediate breaking changes.`,
+ ),
+ );
+ configs.push(
+ await createConfig(publicPaths, {
+ ...commonConfigOptions,
+ publicSubPath: '/public',
+ }),
+ );
+ }
const isCi = yn(process.env.CI, { default: false });
const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist);
+ const previousAuthSizes = publicPaths
+ ? await measureFileSizesBeforeBuild(publicPaths.targetDist)
+ : undefined;
await fs.emptyDir(paths.targetDist);
if (paths.targetPublic) {
@@ -76,33 +102,43 @@ export async function buildBundle(options: BuildOptions) {
);
}
- const { stats } = await build(config, isCi);
+ const { stats } = await build(configs, isCi);
if (!stats) {
throw new Error('No stats returned');
}
+ const [mainStats, authStats] = stats.stats;
if (statsJsonEnabled) {
// No @types/bfj
await require('bfj').write(
resolvePath(paths.targetDist, 'bundle-stats.json'),
- stats.toJson(),
+ mainStats.toJson(),
);
}
printFileSizesAfterBuild(
- stats,
+ mainStats,
previousFileSizes,
paths.targetDist,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE,
);
+ if (publicPaths && previousAuthSizes) {
+ printFileSizesAfterBuild(
+ authStats,
+ previousAuthSizes,
+ publicPaths.targetDist,
+ WARN_AFTER_BUNDLE_GZIP_SIZE,
+ WARN_AFTER_CHUNK_GZIP_SIZE,
+ );
+ }
}
-async function build(config: webpack.Configuration, isCi: boolean) {
- const stats = await new Promise(
+async function build(configs: webpack.Configuration[], isCi: boolean) {
+ const stats = await new Promise(
(resolve, reject) => {
- webpack(config, (err, buildStats) => {
+ webpack(configs, (err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts
index a9d5c2aa67..7c72dc81f7 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/lib/bundler/config.ts
@@ -92,7 +92,7 @@ export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): Promise {
- const { checksEnabled, isDev, frontendConfig } = options;
+ const { checksEnabled, isDev, frontendConfig, publicSubPath = '' } = options;
const { plugins, loaders } = transforms(options);
// Any package that is part of the monorepo but outside the monorepo root dir need
@@ -102,7 +102,11 @@ export async function createConfig(
const baseUrl = frontendConfig.getString('app.baseUrl');
const validBaseUrl = new URL(baseUrl);
- const publicPath = validBaseUrl.pathname.replace(/\/$/, '');
+ let publicPath = validBaseUrl.pathname.replace(/\/$/, '');
+ if (publicSubPath) {
+ publicPath = `${publicPath}${publicSubPath}`.replace('//', '/');
+ }
+
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts
index 3d4925c66b..572a96337f 100644
--- a/packages/cli/src/lib/bundler/paths.ts
+++ b/packages/cli/src/lib/bundler/paths.ts
@@ -23,6 +23,8 @@ export type BundlingPathsOptions = {
entry: string;
// Target directory, defaulting to paths.targetDir
targetDir?: string;
+ // Relative dist directory, defaulting to 'dist'
+ dist?: string;
};
export function resolveBundlingPaths(options: BundlingPathsOptions) {
@@ -60,7 +62,7 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
targetPublic,
targetPath: resolvePath(targetDir, '.'),
targetRunFile: runFileExists ? targetRunFile : undefined,
- targetDist: resolvePath(targetDir, 'dist'),
+ targetDist: resolvePath(targetDir, options.dist ?? 'dist'),
targetAssets: resolvePath(targetDir, 'assets'),
targetSrc: resolvePath(targetDir, 'src'),
targetDev: resolvePath(targetDir, 'dev'),
@@ -72,4 +74,14 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
};
}
+export async function resolveOptionalBundlingPaths(
+ options: BundlingPathsOptions,
+) {
+ const resolvedPaths = resolveBundlingPaths(options);
+ if (await fs.pathExists(resolvedPaths.targetEntry)) {
+ return resolvedPaths;
+ }
+ return undefined;
+}
+
export type BundlingPaths = ReturnType;
diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts
index 2fd1385584..f1eb2e923d 100644
--- a/packages/cli/src/lib/bundler/server.ts
+++ b/packages/cli/src/lib/bundler/server.ts
@@ -32,7 +32,7 @@ import { loadCliConfig } from '../config';
import { Lockfile } from '../versioning';
import { createConfig, resolveBaseUrl } from './config';
import { createDetectedModulesEntryPoint } from './packageDetection';
-import { resolveBundlingPaths } from './paths';
+import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
import { ServeOptions } from './types';
import { hasReactDomClient } from './hasReactDomClient';
@@ -147,7 +147,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
},
});
- const config = await createConfig(paths, {
+ const commonConfigOptions = {
...options,
checksEnabled: options.checksEnabled,
isDev: true,
@@ -156,6 +156,10 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
getFrontendAppConfigs: () => {
return latestFrontendAppConfigs;
},
+ };
+
+ const config = await createConfig(paths, {
+ ...commonConfigOptions,
additionalEntryPoints: detectedModulesEntryPoint,
});
@@ -199,7 +203,26 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
root: paths.targetPath,
});
} else {
- const compiler = webpack(config);
+ const publicPaths = await resolveOptionalBundlingPaths({
+ entry: 'src/index-public-experimental',
+ dist: 'dist/public',
+ });
+ if (publicPaths) {
+ console.log(
+ chalk.yellow(
+ `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
+ ),
+ );
+ }
+ const compiler = publicPaths
+ ? webpack([
+ config,
+ await createConfig(publicPaths, {
+ ...commonConfigOptions,
+ publicSubPath: '/public',
+ }),
+ ])
+ : webpack(config);
webpackServer = new WebpackDevServer(
{
diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts
index 414414fa81..4a9424dedb 100644
--- a/packages/cli/src/lib/bundler/types.ts
+++ b/packages/cli/src/lib/bundler/types.ts
@@ -26,6 +26,8 @@ export type BundlingOptions = {
baseUrl: URL;
parallelism?: number;
additionalEntryPoints?: string[];
+ // Path to append to the detected public path, e.g. '/public'
+ publicSubPath?: string;
};
export type ServeOptions = BundlingPathsOptions & {