Merge pull request #22434 from backstage/rugvip/authentry

cli: add support for a separate /public entry point
This commit is contained in:
Patrik Oldsberg
2024-01-23 13:58:21 +01:00
committed by GitHub
7 changed files with 179 additions and 15 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Add experimental support for optional `auth` app entry point.
@@ -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 (
<SignInPage
{...props}
providers={['guest', 'custom', ...providers]}
title="Select a sign-in method"
align="center"
/>
);
},
},
});
function RedirectToRoot() {
window.location.pathname = readBasePath(useApi(configApiRef));
return <div />;
}
const App = app.createRoot(
<>
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<RedirectToRoot />
</AppRouter>
</>,
);
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
+45 -9
View File
@@ -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<webpack.Stats | undefined>(
async function build(configs: webpack.Configuration[], isCi: boolean) {
const stats = await new Promise<webpack.MultiStats | undefined>(
(resolve, reject) => {
webpack(config, (err, buildStats) => {
webpack(configs, (err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
+6 -2
View File
@@ -92,7 +92,7 @@ export async function createConfig(
paths: BundlingPaths,
options: BundlingOptions,
): Promise<webpack.Configuration> {
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({
+13 -1
View File
@@ -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<typeof resolveBundlingPaths>;
+26 -3
View File
@@ -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(
{
+2
View File
@@ -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 & {