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 & {