cli: add initial experimental support for dynamic plugins
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
"@backstage/release-manifests": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@module-federation/enhanced": "^0.1.19",
|
||||
"@octokit/graphql": "^5.0.0",
|
||||
"@octokit/graphql-schema": "^13.7.0",
|
||||
"@octokit/oauth-app": "^4.2.0",
|
||||
|
||||
@@ -24,6 +24,7 @@ interface BuildAppOptions {
|
||||
targetDir: string;
|
||||
writeStats: boolean;
|
||||
configPaths: string[];
|
||||
moduleFederationMode?: 'host' | 'remote';
|
||||
}
|
||||
|
||||
export async function buildFrontend(options: BuildAppOptions) {
|
||||
@@ -34,6 +35,13 @@ export async function buildFrontend(options: BuildAppOptions) {
|
||||
entry: 'src/index',
|
||||
parallelism: getEnvironmentParallelism(),
|
||||
statsJsonEnabled: writeStats,
|
||||
moduleFederation: options.moduleFederationMode && {
|
||||
// The default output mode requires the name to be a usable as a code
|
||||
// symbol, there might be better options here but for now we need to
|
||||
// sanitize the name.
|
||||
name: name.replaceAll('@', '').replaceAll(/[/\\_-]/g, '_'),
|
||||
mode: options.moduleFederationMode,
|
||||
},
|
||||
...(await loadCliConfig({
|
||||
args: configPaths,
|
||||
fromPackage: name,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { paths } from '../../lib/paths';
|
||||
import { buildFrontend } from './buildFrontend';
|
||||
import { buildBackend } from './buildBackend';
|
||||
import { isValidUrl } from '../../lib/urls';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
const role = await findRoleFromCommand(opts);
|
||||
@@ -39,6 +40,9 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
targetDir: paths.targetDir,
|
||||
configPaths,
|
||||
writeStats: Boolean(opts.stats),
|
||||
moduleFederationMode: process.env.EXPERIMENTAL_MODULE_FEDERATION
|
||||
? 'host'
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return buildBackend({
|
||||
@@ -49,6 +53,21 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// experimental
|
||||
if ((role as string) === 'frontend-dynamic-container') {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`,
|
||||
),
|
||||
);
|
||||
return buildFrontend({
|
||||
targetDir: paths.targetDir,
|
||||
configPaths: [],
|
||||
writeStats: Boolean(opts.stats),
|
||||
moduleFederationMode: 'remote',
|
||||
});
|
||||
}
|
||||
|
||||
const roleInfo = PackageRoles.getRoleInfo(role);
|
||||
|
||||
const outputs = new Set<Output>();
|
||||
|
||||
@@ -46,37 +46,45 @@ export async function buildBundle(options: BuildOptions) {
|
||||
dist: 'dist/public',
|
||||
});
|
||||
|
||||
const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({
|
||||
config: options.fullConfig,
|
||||
targetPath: paths.targetPath,
|
||||
});
|
||||
|
||||
const commonConfigOptions = {
|
||||
...options,
|
||||
checksEnabled: false,
|
||||
isDev: false,
|
||||
getFrontendAppConfigs: () => options.frontendAppConfigs,
|
||||
};
|
||||
const configs = [
|
||||
await createConfig(paths, {
|
||||
...commonConfigOptions,
|
||||
additionalEntryPoints: detectedModulesEntryPoint,
|
||||
appMode: publicPaths ? 'protected' : 'public',
|
||||
}),
|
||||
];
|
||||
|
||||
if (publicPaths) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
|
||||
),
|
||||
);
|
||||
const configs = [];
|
||||
|
||||
if (options.moduleFederation?.mode === 'remote') {
|
||||
// Package detection is disabled for remote bundles
|
||||
configs.push(await createConfig(paths, commonConfigOptions));
|
||||
} else {
|
||||
const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({
|
||||
config: options.fullConfig,
|
||||
targetPath: paths.targetPath,
|
||||
});
|
||||
|
||||
configs.push(
|
||||
await createConfig(publicPaths, {
|
||||
await createConfig(paths, {
|
||||
...commonConfigOptions,
|
||||
appMode: 'public',
|
||||
additionalEntryPoints: detectedModulesEntryPoint,
|
||||
appMode: publicPaths ? 'protected' : 'public',
|
||||
}),
|
||||
);
|
||||
|
||||
if (publicPaths) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
|
||||
),
|
||||
);
|
||||
configs.push(
|
||||
await createConfig(publicPaths, {
|
||||
...commonConfigOptions,
|
||||
appMode: 'public',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isCi = yn(process.env.CI, { default: false });
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Config } from '@backstage/config';
|
||||
import ESLintPlugin from 'eslint-webpack-plugin';
|
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import { ModuleFederationPlugin } from '@module-federation/enhanced';
|
||||
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
|
||||
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
|
||||
import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin';
|
||||
@@ -128,18 +129,81 @@ export async function createConfig(
|
||||
}),
|
||||
);
|
||||
|
||||
plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
meta: {
|
||||
'backstage-app-mode': options?.appMode ?? 'public',
|
||||
},
|
||||
template: paths.targetHtml,
|
||||
templateParameters: {
|
||||
publicPath,
|
||||
config: frontendConfig,
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (options.moduleFederation?.mode !== 'remote') {
|
||||
plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
meta: {
|
||||
'backstage-app-mode': options?.appMode ?? 'public',
|
||||
},
|
||||
template: paths.targetHtml,
|
||||
templateParameters: {
|
||||
publicPath,
|
||||
config: frontendConfig,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.moduleFederation) {
|
||||
plugins.push(
|
||||
new ModuleFederationPlugin({
|
||||
...(options.moduleFederation?.mode === 'remote' && {
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'.': paths.targetEntry,
|
||||
},
|
||||
}),
|
||||
name: options.moduleFederation.name,
|
||||
runtime: false,
|
||||
shared: {
|
||||
// React
|
||||
react: {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
'react-dom': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
// React Router
|
||||
'react-router': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
'react-router-dom': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
// MUI v4
|
||||
'@material-ui/core/styles': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
'@material-ui/styles': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
// MUI v5
|
||||
'@mui/material/styles/': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
'@emotion/react': {
|
||||
singleton: true,
|
||||
requiredVersion: '*',
|
||||
eager: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const buildInfo = await readBuildInfo();
|
||||
plugins.push(
|
||||
@@ -211,8 +275,10 @@ export async function createConfig(
|
||||
rules: loaders,
|
||||
},
|
||||
output: {
|
||||
uniqueName: options.moduleFederation?.name,
|
||||
path: paths.targetDist,
|
||||
publicPath: `${publicPath}/`,
|
||||
publicPath:
|
||||
options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`,
|
||||
filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js',
|
||||
chunkFilename: isDev
|
||||
? '[name].chunk.js'
|
||||
|
||||
@@ -30,6 +30,13 @@ export const optimization = (
|
||||
new EsbuildPlugin({
|
||||
target: 'es2019',
|
||||
format: 'iife',
|
||||
exclude: 'remoteEntry.js',
|
||||
}),
|
||||
// Avoid iife wrapping of module federation remote entry as it breaks the variable assignment
|
||||
new EsbuildPlugin({
|
||||
target: 'es2019',
|
||||
format: undefined,
|
||||
include: 'remoteEntry.js',
|
||||
}),
|
||||
],
|
||||
runtimeChunk: 'single',
|
||||
|
||||
@@ -18,6 +18,13 @@ import { AppConfig, Config } from '@backstage/config';
|
||||
import { BundlingPathsOptions } from './paths';
|
||||
import { ConfigSchema } from '@backstage/config-loader';
|
||||
|
||||
export type ModuleFederationOptions = {
|
||||
// Unique name for this module federation bundle
|
||||
name: string;
|
||||
// Whether this is a host or remote bundle
|
||||
mode: 'host' | 'remote';
|
||||
};
|
||||
|
||||
export type BundlingOptions = {
|
||||
checksEnabled: boolean;
|
||||
isDev: boolean;
|
||||
@@ -29,6 +36,7 @@ export type BundlingOptions = {
|
||||
publicSubPath?: string;
|
||||
// Mode that the app is running in, 'protected' or 'public', default is 'public'
|
||||
appMode?: string;
|
||||
moduleFederation?: ModuleFederationOptions;
|
||||
};
|
||||
|
||||
export type ServeOptions = BundlingPathsOptions & {
|
||||
@@ -46,6 +54,7 @@ export type BuildOptions = BundlingPathsOptions & {
|
||||
frontendConfig: Config;
|
||||
frontendAppConfigs: AppConfig[];
|
||||
fullConfig: Config;
|
||||
moduleFederation?: ModuleFederationOptions;
|
||||
};
|
||||
|
||||
export type BackendBundlingOptions = {
|
||||
|
||||
Reference in New Issue
Block a user