Merge pull request #1139 from spotify/rugvip/bundleconfig

packages/cli: fix handling of public dir and hook it up to config
This commit is contained in:
Patrik Oldsberg
2020-06-08 15:24:06 +02:00
committed by GitHub
20 changed files with 147 additions and 82 deletions
+10 -19
View File
@@ -8,47 +8,38 @@
name="description"
content="Backstage is an open platform for building developer portals"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="manifest"
href="%PUBLIC_URL%/manifest.json"
href="<%= publicPath %>/manifest.json"
crossorigin="use-credentials"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="<%= publicPath %>/favicon.ico" />
<link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/apple-touch-icon.png"
href="<%= publicPath %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon-32x32.png"
href="<%= publicPath %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon-16x16.png"
href="<%= publicPath %>/favicon-16x16.png"
/>
<link
rel="mask-icon"
href="%PUBLIC_URL%/safari-pinned-tab.svg"
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<style>
@@ -56,9 +47,9 @@
min-height: 100%;
}
</style>
<title>Backstage</title>
<title><%= app.title %></title>
</head>
<body style="margin: 0">
<body style="margin: 0;">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
+3 -6
View File
@@ -16,7 +16,6 @@
import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core';
import React, { FC } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Root from './components/Root';
import * as plugins from './plugins';
import apis from './apis';
@@ -34,11 +33,9 @@ const App: FC<{}> = () => (
<AppProvider>
<AlertDisplay />
<OAuthRequestDialog />
<Router>
<Root>
<AppComponent />
</Root>
</Router>
<Root>
<AppComponent />
</Root>
</AppProvider>
);
+1 -1
View File
@@ -61,7 +61,7 @@
"ora": "^4.0.3",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^10.2.0",
"react-dev-utils": "^10.2.1",
"react-hot-loader": "^4.12.21",
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
+5 -2
View File
@@ -14,14 +14,17 @@
* limitations under the License.
*/
import { buildBundle } from '../../lib/bundler';
import { Command } from 'commander';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { buildBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
await buildBundle({
entry: 'src/index',
statsJsonEnabled: cmd.stats,
appConfig: await loadConfig(),
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
};
+5 -2
View File
@@ -15,14 +15,17 @@
*/
import { Command } from 'commander';
import { serveBundle } from '../../lib/bundler';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const waitForExit = await serveBundle({
entry: 'src/index',
checksEnabled: cmd.check,
appConfig: await loadConfig(),
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
await waitForExit();
+5 -2
View File
@@ -15,14 +15,17 @@
*/
import { Command } from 'commander';
import { serveBundle } from '../../lib/bundler';
import { loadConfig } from '@backstage/config-loader';
import { ConfigReader } from '@backstage/config';
import { serveBundle } from '../../lib/bundler';
export default async (cmd: Command) => {
const appConfigs = await loadConfig();
const waitForExit = await serveBundle({
entry: 'dev/index',
checksEnabled: cmd.check,
appConfig: await loadConfig(),
config: ConfigReader.fromConfigs(appConfigs),
appConfigs,
});
await waitForExit();
+7
View File
@@ -48,6 +48,13 @@ export async function buildBundle(options: BuildOptions) {
const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist);
await fs.emptyDir(paths.targetDist);
if (paths.targetPublic) {
await fs.copy(paths.targetPublic, paths.targetDist, {
dereference: true,
filter: file => file !== paths.targetHtml,
});
}
const { stats } = await build(compiler, isCi).catch(error => {
console.log(chalk.red('Failed to compile.\n'));
throw new Error(`Failed to compile.\n${error.message || error}`);
+23 -3
View File
@@ -17,6 +17,7 @@
import webpack from 'webpack';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { BundlingPaths } from './paths';
import { transforms } from './transforms';
import { optimization } from './optimization';
@@ -33,7 +34,13 @@ export function createConfig(
): webpack.Configuration {
const { checksEnabled, isDev } = options;
const { plugins, loaders } = transforms(paths, options);
const { plugins, loaders } = transforms(options);
const baseUrl = options.config.getString('app.baseUrl');
if (!baseUrl) {
throw new Error('app.baseUrl must be set in config');
}
const validBaseUrl = new URL(baseUrl, 'https://backstage-app.dev');
if (checksEnabled) {
plugins.push(
@@ -53,7 +60,20 @@ export function createConfig(
plugins.push(
new webpack.EnvironmentPlugin({
APP_CONFIG: options.appConfig,
APP_CONFIG: options.appConfigs,
}),
);
plugins.push(
new HtmlWebpackPlugin({
template: paths.targetHtml,
templateParameters: {
publicPath: validBaseUrl.pathname.replace(/\/$/, ''),
app: {
title: options.config.getString('app.title'),
baseUrl: validBaseUrl.href,
},
},
}),
);
@@ -85,7 +105,7 @@ export function createConfig(
},
output: {
path: paths.targetDist,
publicPath: '/',
publicPath: validBaseUrl.pathname,
filename: isDev ? '[name].js' : '[name].[hash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
+14 -5
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { existsSync } from 'fs';
import fs from 'fs-extra';
import { paths } from '../paths';
export type BundlingPathsOptions = {
@@ -28,20 +28,29 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
const resolveTargetModule = (path: string) => {
for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) {
const filePath = paths.resolveTarget(`${path}.${ext}`);
if (existsSync(filePath)) {
if (fs.pathExistsSync(filePath)) {
return filePath;
}
}
return paths.resolveTarget(`${path}.js`);
};
let targetHtml = paths.resolveTarget(`${entry}.html`);
if (!existsSync(targetHtml)) {
targetHtml = paths.resolveOwn('templates/serve_index.html');
let targetPublic = undefined;
let targetHtml = paths.resolveTarget('public/index.html');
// Prefer public folder
if (fs.pathExistsSync(targetHtml)) {
targetPublic = paths.resolveTarget('public');
} else {
targetHtml = paths.resolveTarget(`${entry}.html`);
if (!fs.pathExistsSync(targetHtml)) {
targetHtml = paths.resolveOwn('templates/serve_index.html');
}
}
return {
targetHtml,
targetPublic,
targetPath: paths.resolveTarget('.'),
targetDist: paths.resolveTarget('dist'),
targetAssets: paths.resolveTarget('assets'),
+16 -2
View File
@@ -34,7 +34,6 @@ export async function serveBundle(options: ServeOptions) {
}
const protocol = yn(process.env.HTTPS, { default: false }) ? 'https' : 'http';
const urls = prepareUrls(protocol, host, port);
const paths = resolveBundlingPaths(options);
const pkgPath = paths.targetPackageJson;
@@ -44,7 +43,9 @@ export async function serveBundle(options: ServeOptions) {
const server = new WebpackDevServer(compiler, {
hot: true,
publicPath: '/',
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
historyApiFallback: true,
clientLogLevel: 'warning',
stats: 'errors-warnings',
@@ -61,6 +62,19 @@ export async function serveBundle(options: ServeOptions) {
return;
}
// TODO: This signature is available in 10.2.1 but doesn't have types published yet
const latestPrepareUrls = prepareUrls as (
protocol: string,
host: string,
port: number,
path?: string,
) => ReturnType<typeof prepareUrls>;
const urls = latestPrepareUrls(
protocol,
host,
port,
config.output?.publicPath,
);
openBrowser(urls.localUrlForBrowser);
resolve();
});
+1 -12
View File
@@ -15,20 +15,15 @@
*/
import webpack, { Module, Plugin } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { BundlingOptions } from './types';
import { BundlingPaths } from './paths';
type Transforms = {
loaders: Module['rules'];
plugins: Plugin[];
};
export const transforms = (
paths: BundlingPaths,
options: BundlingOptions,
): Transforms => {
export const transforms = (options: BundlingOptions): Transforms => {
const { isDev } = options;
const loaders = [
@@ -80,12 +75,6 @@ export const transforms = (
const plugins = new Array<Plugin>();
plugins.push(
new HtmlWebpackPlugin({
template: paths.targetHtml,
}),
);
if (isDev) {
plugins.push(new webpack.HotModuleReplacementPlugin());
} else {
+7 -4
View File
@@ -14,21 +14,24 @@
* limitations under the License.
*/
import { AppConfig } from '@backstage/config';
import { AppConfig, Config } from '@backstage/config';
import { BundlingPathsOptions } from './paths';
export type BundlingOptions = {
checksEnabled: boolean;
isDev: boolean;
appConfig: AppConfig[];
config: Config;
appConfigs: AppConfig[];
};
export type ServeOptions = BundlingPathsOptions & {
checksEnabled: boolean;
appConfig: AppConfig[];
config: Config;
appConfigs: AppConfig[];
};
export type BuildOptions = BundlingPathsOptions & {
statsJsonEnabled: boolean;
appConfig: AppConfig[];
config: Config;
appConfigs: AppConfig[];
};
@@ -1,5 +1,6 @@
app:
title: Scaffolded Backstage App
baseUrl: http://localhost:3000
organization:
name: Acme Corporation
@@ -1,7 +1,6 @@
import { makeStyles } from '@material-ui/core';
import { createApp } from '@backstage/core';
import React, { FC } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import * as plugins from './plugins';
const useStyles = makeStyles(theme => ({
@@ -33,9 +32,7 @@ const App: FC<{}> = () => {
useStyles();
return (
<AppProvider>
<Router>
<AppComponent />
</Router>
<AppComponent />
</AppProvider>
);
};
+34 -6
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ComponentType, FC } from 'react';
import React, { ComponentType, FC, useMemo } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
import { BackstageApp, AppComponents, AppConfigLoader } from './types';
@@ -161,30 +161,58 @@ export class PrivateAppImpl implements BackstageApp {
getProvider(): ComponentType<{}> {
const Provider: FC<{}> = ({ children }) => {
const appThemeApi = useMemo(
() => AppThemeSelector.createWithStorage(this.themes),
[],
);
// Keeping this synchronous when a config loader isn't set simplifies tests a lot
const hasConfig = Boolean(this.configLoader);
const config = useAsync(this.configLoader || (() => Promise.resolve([])));
let childNode = children;
let noConfigNode = undefined;
if (hasConfig && config.loading) {
const { Progress } = this.components;
childNode = <Progress />;
noConfigNode = <Progress />;
} else if (config.error) {
const { BootErrorPage } = this.components;
childNode = <BootErrorPage step="load-config" error={config.error} />;
noConfigNode = (
<BootErrorPage step="load-config" error={config.error} />
);
}
// Before the config is loaded we can't use a router, so exit early
if (noConfigNode) {
return (
<ApiProvider apis={ApiRegistry.from([[appThemeApiRef, appThemeApi]])}>
<AppThemeProvider>{noConfigNode}</AppThemeProvider>
</ApiProvider>
);
}
const configReader = ConfigReader.fromConfigs(config.value ?? []);
const appApis = ApiRegistry.from([
[appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)],
[configApiRef, ConfigReader.fromConfigs(config.value ?? [])],
[configApiRef, configReader],
]);
const apis = new ApiAggregator(this.apis, appApis);
const { Router } = this.components;
let { pathname } = new URL(
configReader.getString('app.baseUrl') ?? '/',
'http://dummy.dev', // baseUrl can be specified as just a path
);
if (pathname.endsWith('/')) {
pathname = pathname.replace(/\/$/, '');
}
return (
<ApiProvider apis={apis}>
<AppContextProvider app={this}>
<AppThemeProvider>{childNode}</AppThemeProvider>
<AppThemeProvider>
<Router basename={pathname}>{children}</Router>
</AppThemeProvider>
</AppContextProvider>
</ApiProvider>
);
+1
View File
@@ -30,6 +30,7 @@ export type AppComponents = {
NotFoundErrorPage: ComponentType<{}>;
BootErrorPage: ComponentType<BootErrorPageProps>;
Progress: ComponentType<{}>;
Router: ComponentType<{ basename?: string }>;
};
/**
+4 -3
View File
@@ -22,7 +22,7 @@ import privateExports, {
BootErrorPageProps,
AppConfigLoader,
} from '@backstage/core-api';
import { BrowserRouter as Router } from 'react-router-dom';
import { BrowserRouter, MemoryRouter } from 'react-router-dom';
import { ErrorPage } from '../layout/ErrorPage';
import Progress from '../components/Progress';
@@ -87,9 +87,9 @@ export function createApp(options?: AppOptions) {
}
// TODO: figure out a nicer way to handle routing on the error page, when it can be done.
return (
<Router>
<MemoryRouter>
<ErrorPage status="501" statusMessage={message} />
</Router>
</MemoryRouter>
);
};
@@ -100,6 +100,7 @@ export function createApp(options?: AppOptions) {
NotFoundErrorPage: DefaultNotFoundPage,
BootErrorPage: DefaultBootErrorPage,
Progress: Progress,
Router: BrowserRouter,
...options?.components,
};
const themes = options?.themes ?? [
+4 -7
View File
@@ -17,7 +17,6 @@
import { hot } from 'react-hot-loader/root';
import React, { FC, ComponentType, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import BookmarkIcon from '@material-ui/icons/Bookmark';
import {
createApp,
@@ -94,12 +93,10 @@ class DevAppBuilder {
<AlertDisplay />
<OAuthRequestDialog />
{this.rootChildren}
<BrowserRouter>
<SidebarPage>
{sidebar}
<AppComponent />
</SidebarPage>
</BrowserRouter>
<SidebarPage>
{sidebar}
<AppComponent />
</SidebarPage>
</AppProvider>
);
};
@@ -55,6 +55,9 @@ export function wrapInTestApp(
NotFoundErrorPage,
BootErrorPage,
Progress,
Router: ({ children }) => (
<MemoryRouter initialEntries={routeEntries} children={children} />
),
},
icons: defaultSystemIcons,
plugins: [],
@@ -79,9 +82,7 @@ export function wrapInTestApp(
return (
<AppProvider>
<MemoryRouter initialEntries={routeEntries}>
<Route component={Wrapper} />
</MemoryRouter>
<Route component={Wrapper} />
</AppProvider>
);
}
+1 -1
View File
@@ -15290,7 +15290,7 @@ react-clientside-effect@^1.2.2:
dependencies:
"@babel/runtime" "^7.0.0"
react-dev-utils@^10.2.0:
react-dev-utils@^10.2.1:
version "10.2.1"
resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz#f6de325ae25fa4d546d09df4bb1befdc6dd19c19"
integrity sha512-XxTbgJnYZmxuPtY3y/UV0D8/65NKkmaia4rXzViknVnZeVlklSh8u6TnaEYPfAi/Gh1TP4mEOXHI6jQOPbeakQ==