Skip to content

Commit 91e06da

Browse files
Hyperkid123tumido
andauthored
feat(cli): add frontend dynamic plugins base build config (#747)
* feat(cli): build dynamic frontend plugins chore(ci): bump test node version (#831) feat(cli): add frontend dynamic plugins base build config fix(cli): resolve post dynamic build stats errors fix(cli): remove eager consumption settings from shared modules Eager packages will always end up within the container entry JS files which significantly increases the network bandwidth even if packages the shared packages have been already initialized in the shared scope via different container. fix(cli): remove the requirement for root tsconfig.json chore(dynamic-plugins): add sample dynamic plugin package chore(app-next): add sample app with dynamic plugins fix(cli): align versions of @backstage/backend-common package feat(app-next): dynamically load entire backstage plugins chore(app-next): add readme chore(app-next): fix eslint errors fix(cli): do not treat warnings as errors on CI chore(dependencies): align dependency versions * chore(cli): cleanup and enable quay dynamic build Signed-off-by: Tomas Coufal <[email protected]> --------- Signed-off-by: Tomas Coufal <[email protected]> Co-authored-by: Tomas Coufal <[email protected]>
1 parent 04b2d07 commit 91e06da

File tree

18 files changed

+717
-161
lines changed

18 files changed

+717
-161
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ node_modules/
3131
# Build output
3232
dist
3333
dist-types
34+
dist-scalprum
3435

3536
# Temporary change files created by Vim
3637
*.swp
@@ -49,3 +50,6 @@ site
4950

5051
# turbo
5152
.turbo
53+
54+
# build cache
55+
.webpack-cache

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
"preinstall": "npx only-allow yarn",
1010
"start": "turbo run start",
1111
"start:backstage": "turbo run start --filter=...{./packages/app} --filter=...{./packages/backend}",
12-
"start:plugins": "turbo run start --filter=...{./plugins/*}",
12+
"start:plugins": "turbo run start --filter=@janus-idp/*",
1313
"build": "turbo run build",
14-
"build:backstage": "turbo run build --filter=...{./packages/*}",
15-
"build:plugins": "turbo run build --filter=...{./plugins/*}",
14+
"build:backstage": "turbo run build --filter=...{./packages/app} --filter=...{./packages/backend}",
15+
"build:plugins": "turbo run build --filter=@janus-idp/*",
1616
"tsc": "turbo run tsc",
1717
"clean": "turbo run clean",
1818
"test": "turbo run test",

packages/cli/bin/janus-cli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
2424
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
2525
require('..');
2626
} else {
27+
/**
28+
* TODO: Figure out of we need to find `project` path leading to a local plugin
29+
* tsconfig.json.
30+
* This will become relevant once we start migration plugins to the Janus cli that have
31+
* different tsconfig.json specifications.
32+
* */
2733
require('ts-node').register({
2834
transpileOnly: true,
2935
/* eslint-disable-next-line no-restricted-syntax */

packages/cli/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@backstage/integration": "^1.7.1",
4242
"@backstage/release-manifests": "^0.0.10",
4343
"@backstage/types": "^1.1.1",
44+
"@openshift/dynamic-plugin-sdk-webpack": "^3.0.0",
4445
"@esbuild-kit/cjs-loader": "^2.4.4",
4546
"@esbuild-kit/esm-loader": "^2.6.5",
4647
"@manypkg/get-packages": "^1.1.3",
@@ -119,7 +120,7 @@
119120
"react-refresh": "^0.14.0",
120121
"recursive-readdir": "^2.2.2",
121122
"replace-in-file": "^6.0.0",
122-
"rollup": "^2.60.2",
123+
"rollup": "^2.78.0",
123124
"rollup-plugin-dts": "^4.0.1",
124125
"rollup-plugin-esbuild": "^4.7.2",
125126
"rollup-plugin-postcss": "^4.0.0",
@@ -137,7 +138,7 @@
137138
"webpack-node-externals": "^3.0.0",
138139
"yaml": "^2.3.3",
139140
"yml-loader": "^2.1.0",
140-
"yn": "^5.0.0",
141+
"yn": "^4.0.0",
141142
"zod": "^3.22.4"
142143
},
143144
"devDependencies": {

packages/cli/src/commands/build/buildFrontend.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { PluginBuildMetadata } from '@openshift/dynamic-plugin-sdk-webpack';
1718
import fs from 'fs-extra';
1819

1920
import { resolve as resolvePath } from 'path';
@@ -26,16 +27,18 @@ interface BuildAppOptions {
2627
targetDir: string;
2728
writeStats: boolean;
2829
configPaths: string[];
30+
pluginMetadata?: PluginBuildMetadata;
2931
}
3032

3133
export async function buildFrontend(options: BuildAppOptions) {
32-
const { targetDir, writeStats, configPaths } = options;
34+
const { targetDir, writeStats, configPaths, pluginMetadata } = options;
3335
const { name } = await fs.readJson(resolvePath(targetDir, 'package.json'));
3436
await buildBundle({
3537
targetDir,
3638
entry: 'src/index',
3739
parallelism: getEnvironmentParallelism(),
3840
statsJsonEnabled: writeStats,
41+
pluginMetadata,
3942
...(await loadCliConfig({
4043
args: configPaths,
4144
fromPackage: name,

packages/cli/src/commands/export-dynamic-plugin/frontend.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,34 @@
1717
import { PackageRoleInfo } from '@backstage/cli-node';
1818

1919
import { OptionValues } from 'commander';
20+
import fs from 'fs-extra';
21+
22+
import { buildScalprumPlugin } from '../../lib/builder/buildScalprumPlugin';
23+
import { paths } from '../../lib/paths';
2024

2125
export async function frontend(
2226
_: PackageRoleInfo,
2327
__: OptionValues,
2428
): Promise<void> {
25-
throw new Error('frontend not yet implemented');
29+
const { name, version, scalprum } = await fs.readJson(
30+
paths.resolveTarget('package.json'),
31+
);
32+
if (scalprum === undefined) {
33+
throw new Error(
34+
`Package doesn't seem to support dynamic loading. It should have a 'scalprum' key in 'package.json' containing the dynamic loading entrypoints.`,
35+
);
36+
}
37+
38+
await fs.remove(paths.resolveTarget('dist-scalprum'));
39+
40+
await buildScalprumPlugin({
41+
writeStats: false,
42+
configPaths: [],
43+
targetDir: paths.targetDir,
44+
pluginMetadata: {
45+
...scalprum,
46+
version,
47+
},
48+
fromPackage: name,
49+
});
2650
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { PluginBuildMetadata } from '@openshift/dynamic-plugin-sdk-webpack';
2+
3+
import { buildScalprumBundle } from '../bundler/bundlePlugin';
4+
import { loadCliConfig } from '../config';
5+
import { getEnvironmentParallelism } from '../parallel';
6+
7+
interface BuildScalprumPluginOptions {
8+
targetDir: string;
9+
writeStats: boolean;
10+
configPaths: string[];
11+
pluginMetadata: PluginBuildMetadata;
12+
fromPackage: string;
13+
}
14+
15+
export async function buildScalprumPlugin(options: BuildScalprumPluginOptions) {
16+
const { targetDir, pluginMetadata, fromPackage } = options;
17+
await buildScalprumBundle({
18+
targetDir,
19+
entry: 'src/index',
20+
parallelism: getEnvironmentParallelism(),
21+
pluginMetadata,
22+
...(await loadCliConfig({
23+
args: [],
24+
fromPackage,
25+
})),
26+
});
27+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {
2+
measureFileSizesBeforeBuild,
3+
printFileSizesAfterBuild,
4+
} from 'react-dev-utils/FileSizeReporter';
5+
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
6+
7+
import chalk from 'chalk';
8+
import fs from 'fs-extra';
9+
import webpack from 'webpack';
10+
import yn from 'yn';
11+
12+
import { resolveBaseUrl } from './config';
13+
import { BundlingPathsOptions, resolveBundlingPaths } from './paths';
14+
import { createScalprumConfig } from './scalprumConfig';
15+
import { DynamicPluginOptions } from './types';
16+
17+
// TODO(Rugvip): Limits from CRA, we might want to tweak these though.
18+
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
19+
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
20+
21+
function applyContextToError(error: string, moduleName: string): string {
22+
return `Failed to compile '${moduleName}':\n ${error}`;
23+
}
24+
25+
export async function buildScalprumBundle(
26+
options: BundlingPathsOptions & DynamicPluginOptions,
27+
) {
28+
const paths = resolveBundlingPaths(options);
29+
const config = await createScalprumConfig(paths, {
30+
...options,
31+
checksEnabled: false,
32+
isDev: false,
33+
baseUrl: resolveBaseUrl(options.frontendConfig),
34+
});
35+
36+
const isCi = yn(process.env.CI, { default: false });
37+
38+
const previousFileSizes = await measureFileSizesBeforeBuild(
39+
paths.targetScalprumDist,
40+
);
41+
await fs.emptyDir(paths.targetScalprumDist);
42+
43+
if (paths.targetPublic) {
44+
await fs.copy(paths.targetPublic, paths.targetDist, {
45+
dereference: true,
46+
filter: file => file !== paths.targetHtml,
47+
});
48+
}
49+
50+
const { stats } = await build(config, isCi);
51+
52+
if (!stats) {
53+
throw new Error('No stats returned');
54+
}
55+
56+
printFileSizesAfterBuild(
57+
stats,
58+
previousFileSizes,
59+
paths.targetScalprumDist,
60+
WARN_AFTER_BUNDLE_GZIP_SIZE,
61+
WARN_AFTER_CHUNK_GZIP_SIZE,
62+
);
63+
}
64+
65+
async function build(config: webpack.Configuration, isCi: boolean) {
66+
const stats = await new Promise<webpack.Stats | undefined>(
67+
(resolve, reject) => {
68+
webpack(config, (err, buildStats) => {
69+
if (err) {
70+
if (err.message) {
71+
const { errors } = formatWebpackMessages({
72+
errors: [err.message],
73+
warnings: new Array<string>(),
74+
_showErrors: true,
75+
_showWarnings: true,
76+
});
77+
78+
throw new Error(errors[0]);
79+
} else {
80+
reject(err);
81+
}
82+
} else {
83+
resolve(buildStats);
84+
}
85+
});
86+
},
87+
);
88+
89+
if (!stats) {
90+
throw new Error('Failed to compile: No stats provided');
91+
}
92+
93+
const serializedStats = stats.toJson({
94+
all: false,
95+
warnings: true,
96+
errors: true,
97+
});
98+
const { errors, warnings } = formatWebpackMessages({
99+
errors: serializedStats.errors,
100+
warnings: serializedStats.warnings,
101+
});
102+
103+
if (errors.length) {
104+
// Only keep the first error. Others are often indicative
105+
// of the same problem, but confuse the reader with noise.
106+
const errorWithContext = applyContextToError(
107+
errors[0],
108+
serializedStats.errors?.[0]?.moduleName ?? '',
109+
);
110+
throw new Error(errorWithContext);
111+
}
112+
if (isCi && warnings.length) {
113+
const warningsWithContext = warnings.map((warning, i) => {
114+
return applyContextToError(
115+
warning,
116+
serializedStats.warnings?.[i]?.moduleName ?? '',
117+
);
118+
});
119+
console.log(chalk.yellow(warningsWithContext.join('\n\n')));
120+
}
121+
122+
return { stats };
123+
}

packages/cli/src/lib/bundler/config.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ import fs from 'fs-extra';
2727
import HtmlWebpackPlugin from 'html-webpack-plugin';
2828
import pickBy from 'lodash/pickBy';
2929
import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin';
30-
import webpack, { ProvidePlugin } from 'webpack';
30+
import webpack, { container, ProvidePlugin } from 'webpack';
3131
import nodeExternals from 'webpack-node-externals';
3232
import yn from 'yn';
3333

34-
import { posix as posixPath, resolve as resolvePath } from 'path';
34+
import {
35+
join as joinPath,
36+
posix as posixPath,
37+
resolve as resolvePath,
38+
} from 'path';
3539

3640
import { paths as cliPaths } from '../../lib/paths';
3741
import { version } from '../../lib/version';
@@ -40,9 +44,18 @@ import { runPlain } from '../run';
4044
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
4145
import { optimization } from './optimization';
4246
import { BundlingPaths } from './paths';
47+
import { sharedModules } from './scalprumConfig';
4348
import { transforms } from './transforms';
4449
import { BackendBundlingOptions, BundlingOptions } from './types';
4550

51+
const { ModuleFederationPlugin } = container;
52+
53+
const scalprumPlugin = new ModuleFederationPlugin({
54+
name: 'backstageHost',
55+
filename: 'backstageHost.[fullhash].js',
56+
shared: [sharedModules],
57+
});
58+
4659
const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE';
4760

4861
export function resolveBaseUrl(config: Config): URL {
@@ -142,6 +155,8 @@ export async function createConfig(
142155
}),
143156
);
144157

158+
plugins.push(scalprumPlugin);
159+
145160
// These files are required by the transpiled code when using React Refresh.
146161
// They need to be excluded to the module scope plugin which ensures that files
147162
// that exist in the package are required.
@@ -156,6 +171,11 @@ export async function createConfig(
156171
const withCache = yn(process.env[BUILD_CACHE_ENV_VAR], { default: false });
157172

158173
return {
174+
cache: {
175+
type: 'filesystem',
176+
allowCollectingMemory: true,
177+
cacheDirectory: joinPath(process.cwd(), '.webpack-cache'),
178+
},
159179
mode: isDev ? 'development' : 'production',
160180
profile: false,
161181
optimization: optimization(options),
@@ -167,6 +187,16 @@ export async function createConfig(
167187
context: paths.targetPath,
168188
entry: [...(options.additionalEntryPoints ?? []), paths.targetEntry],
169189
resolve: {
190+
alias: {
191+
'@backstage/frontend-app-api/src': joinPath(
192+
process.cwd(),
193+
'src',
194+
'overrides',
195+
'@backstage',
196+
'frontend-app-api',
197+
'src',
198+
),
199+
},
170200
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx', '.json', '.wasm'],
171201
mainFields: ['browser', 'module', 'main'],
172202
fallback: {

packages/cli/src/lib/bundler/paths.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function resolveBundlingPaths(options: BundlingPathsOptions) {
6363
targetPath: resolvePath(targetDir, '.'),
6464
targetRunFile: runFileExists ? targetRunFile : undefined,
6565
targetDist: resolvePath(targetDir, 'dist'),
66+
targetScalprumDist: resolvePath(targetDir, 'dist-scalprum'),
6667
targetAssets: resolvePath(targetDir, 'assets'),
6768
targetSrc: resolvePath(targetDir, 'src'),
6869
targetDev: resolvePath(targetDir, 'dev'),

0 commit comments

Comments
 (0)