@gravity-ui/app-builder 0.29.3 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -330,3 +330,141 @@ worker.onmessage = ({data: {result}}) => {
330
330
 
331
331
  worker.postMessage({a: 1, b: 2});
332
332
  ```
333
+
334
+ ##### Module Federation
335
+
336
+ Module Federation is a Webpack 5 feature that enables micro-frontend architecture, where JavaScript applications can dynamically load code from each other at runtime.
337
+
338
+ `app-builder` uses `@module-federation/enhanced` for advanced Module Federation support.
339
+
340
+ - `moduleFederation` (`object`) — Module Federation configuration
341
+ - `name` (`string`) — unique name of the application in the Module Federation ecosystem. Required parameter.
342
+ - `version` (`string`) — application version. When specified, the entry file will be named `entry-{version}.js` instead of `entry.js`.
343
+ - `publicPath` (`string`) — base URL for loading resources of this micro-frontend. Required parameter.
344
+ - `remotes` (`string[]`) — list of remote application names that this application can load. Simplified alternative to `originalRemotes`.
345
+ - `originalRemotes` (`RemotesObject`) — full configuration of remote applications in Module Federation Plugin format.
346
+ - `remotesRuntimeVersioning` (`boolean`) — enables runtime versioning for remote applications.
347
+ - `isolateStyles` (`object`) — CSS style isolation settings to prevent conflicts between micro-frontends.
348
+ - `getPrefix` (`(entryName: string) => string`) — function to generate CSS class prefix.
349
+ - `prefixSelector` (`(prefix: string, selector: string, prefixedSelector: string, filePath: string) => string`) — function to add prefix to CSS selectors.
350
+ - Also supports all standard options from [@module-federation/enhanced](https://module-federation.io/), except `name` and `remotes`, such as:
351
+ - `filename` — entry file name (default `remoteEntry.js`)
352
+ - `exposes` — modules that this application exports
353
+ - `shared` — shared dependencies between applications
354
+ - `runtimePlugins` — plugins for Module Federation runtime
355
+
356
+ **Host Application Configuration Example:**
357
+
358
+ Host applications consume remote modules from other micro-frontends:
359
+
360
+ ```ts
361
+ export default defineConfig({
362
+ client: {
363
+ moduleFederation: {
364
+ name: 'shell',
365
+ publicPath: 'https://cdn.example.com/my-app/',
366
+ // Simple remotes configuration
367
+ remotes: ['header', 'footer', 'sidebar'],
368
+ shared: {
369
+ react: {singleton: true, requiredVersion: '^18.0.0'},
370
+ 'react-dom': {singleton: true, requiredVersion: '^18.0.0'},
371
+ lodash: {singleton: true},
372
+ },
373
+ },
374
+ },
375
+ });
376
+ ```
377
+
378
+ **Advanced Host Configuration:**
379
+
380
+ ```ts
381
+ export default defineConfig({
382
+ client: {
383
+ moduleFederation: {
384
+ name: 'main-shell',
385
+ version: '2.1.0',
386
+ publicPath: 'https://cdn.example.com/my-app/',
387
+ // Detailed remotes configuration
388
+ originalRemotes: {
389
+ header: 'header@https://cdn.example.com/header/remoteEntry.js',
390
+ footer: 'footer@https://cdn.example.com/footer/remoteEntry.js',
391
+ userProfile: 'userProfile@https://cdn.example.com/user-profile/remoteEntry.js',
392
+ },
393
+ remotesRuntimeVersioning: true,
394
+ isolateStyles: {
395
+ getPrefix: (entryName) => `.app-${entryName}`,
396
+ prefixSelector: (prefix, selector, prefixedSelector, filePath) => {
397
+ if (
398
+ [prefix, ':root', 'html', 'body', '.g-root', '.remote-app'].some((item) =>
399
+ selector.startsWith(item),
400
+ ) ||
401
+ filePath.includes('@gravity-ui/chartkit')
402
+ ) {
403
+ return selector;
404
+ }
405
+ return prefixedSelector;
406
+ },
407
+ },
408
+ shared: {
409
+ react: {singleton: true, requiredVersion: '^18.0.0'},
410
+ 'react-dom': {singleton: true, requiredVersion: '^18.0.0'},
411
+ lodash: {singleton: true},
412
+ },
413
+ },
414
+ },
415
+ });
416
+ ```
417
+
418
+ **Remote Application Configuration Example:**
419
+
420
+ Remote applications expose their modules for consumption by host applications:
421
+
422
+ ```ts
423
+ export default defineConfig({
424
+ client: {
425
+ moduleFederation: {
426
+ name: 'header',
427
+ publicPath: 'https://cdn.example.com/my-app/',
428
+ // Expose modules for other applications
429
+ exposes: {
430
+ './Header': './src/components/Header',
431
+ './Navigation': './src/components/Navigation',
432
+ './UserMenu': './src/components/UserMenu',
433
+ },
434
+ shared: {
435
+ react: {singleton: true, requiredVersion: '^18.0.0'},
436
+ 'react-dom': {singleton: true, requiredVersion: '^18.0.0'},
437
+ lodash: {singleton: true},
438
+ },
439
+ },
440
+ },
441
+ });
442
+ ```
443
+
444
+ **Bidirectional Configuration Example:**
445
+
446
+ Applications can be both host and remote simultaneously:
447
+
448
+ ```ts
449
+ export default defineConfig({
450
+ client: {
451
+ moduleFederation: {
452
+ name: 'dashboard',
453
+ version: '1.5.0',
454
+ publicPath: 'https://cdn.example.com/my-app/',
455
+ // Consume remote modules
456
+ remotes: ['charts', 'notifications'],
457
+ // Expose own modules
458
+ exposes: {
459
+ './DashboardLayout': './src/layouts/DashboardLayout',
460
+ './DataTable': './src/components/DataTable',
461
+ },
462
+ shared: {
463
+ react: {singleton: true, requiredVersion: '^18.0.0'},
464
+ 'react-dom': {singleton: true, requiredVersion: '^18.0.0'},
465
+ lodash: {singleton: true},
466
+ },
467
+ },
468
+ },
469
+ });
470
+ ```
package/dist/cli.js CHANGED
@@ -15,17 +15,17 @@ const { version } = process;
15
15
  if (!semver_1.default.satisfies(version, `>=${MIN_NODE_VERSION}`, {
16
16
  includePrerelease: true,
17
17
  })) {
18
- logger_1.default.panic((0, common_tags_1.stripIndent)(`
18
+ logger_1.default.panic((0, common_tags_1.stripIndent) `
19
19
  App-builder requires Node.js ${MIN_NODE_VERSION} or higher (you have ${version}).
20
20
  Upgrade Node to the latest stable release.
21
- `));
21
+ `);
22
22
  }
23
23
  if (semver_1.default.prerelease(version)) {
24
- logger_1.default.warning((0, common_tags_1.stripIndent)(`
24
+ logger_1.default.warning((0, common_tags_1.stripIndent) `
25
25
  You are currently using a prerelease version of Node (${version}), which is not supported.
26
26
  You can use this for testing, but we do not recommend it in production.
27
27
  Before reporting any bugs, please test with a supported version of Node (>=${MIN_NODE_VERSION}).
28
- `));
28
+ `);
29
29
  }
30
30
  process.on('unhandledRejection', (reason) => {
31
31
  // This will exit the process in newer Node anyway so lets be consistent
@@ -38,7 +38,7 @@ const logger = new Logger('server', ${config.verbose});
38
38
  compile(ts, {logger, projectPath: ${JSON.stringify(paths_1.default.appServer)}});`;
39
39
  }
40
40
  function buildServer(config) {
41
- (0, utils_1.createRunFolder)();
41
+ (0, utils_1.createRunFolder)(config);
42
42
  return new Promise((resolve, reject) => {
43
43
  const build = new controllable_script_1.ControllableScript(config.server.compiler === 'swc'
44
44
  ? createSWCBuildScript(config)
@@ -152,7 +152,7 @@ async function buildDevServer(config) {
152
152
  };
153
153
  const listenOn = options.port || options.ipc;
154
154
  if (!listenOn) {
155
- options.ipc = path.resolve(paths_1.default.appDist, 'run/client.sock');
155
+ options.ipc = path.resolve((0, utils_1.getAppRunPath)(config), 'client.sock');
156
156
  }
157
157
  const proxy = options.proxy || [];
158
158
  if (config.client.lazyCompilation && bundler !== 'rspack') {
@@ -39,13 +39,14 @@ async function default_1(config) {
39
39
  process.env.NODE_ENV = 'development';
40
40
  const shouldCompileClient = (0, utils_1.shouldCompileTarget)(config.target, 'client');
41
41
  const shouldCompileServer = (0, utils_1.shouldCompileTarget)(config.target, 'server');
42
+ const appRunPath = (0, utils_1.getAppRunPath)(config);
42
43
  if (shouldCompileClient && shouldCompileServer) {
43
44
  try {
44
- fs.accessSync(paths_1.default.appRun, fs.constants.W_OK | fs.constants.X_OK); // eslint-disable-line no-bitwise
45
- rimraf_1.rimraf.sync(paths_1.default.appRun);
45
+ fs.accessSync(appRunPath, fs.constants.W_OK | fs.constants.X_OK); // eslint-disable-line no-bitwise
46
+ rimraf_1.rimraf.sync(appRunPath);
46
47
  }
47
48
  catch (error) {
48
- logger_1.default.warning(`Failed to remove appRun path [${paths_1.default.appRun}]: ${error}`);
49
+ logger_1.default.warning(`Failed to remove appRun path [${appRunPath}]: ${error}`);
49
50
  }
50
51
  }
51
52
  let clientCompiled = !shouldCompileClient;
@@ -79,7 +79,7 @@ watch(
79
79
  async function watchServerCompilation(config) {
80
80
  const serverPath = path.resolve(paths_1.default.appDist, 'server');
81
81
  rimraf_1.rimraf.sync(serverPath);
82
- (0, utils_1.createRunFolder)();
82
+ (0, utils_1.createRunFolder)(config);
83
83
  const build = new controllable_script_1.ControllableScript(config.server.compiler === 'swc'
84
84
  ? createSWCBuildScript(config)
85
85
  : createTypescriptBuildScript(config), null);
@@ -22,14 +22,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.getProjectConfig = getProjectConfig;
27
30
  exports.normalizeConfig = normalizeConfig;
28
31
  const path = __importStar(require("node:path"));
29
32
  const cosmiconfig_1 = require("cosmiconfig");
30
33
  const cosmiconfig_typescript_loader_1 = require("cosmiconfig-typescript-loader");
34
+ const common_tags_1 = require("common-tags");
31
35
  const models_1 = require("./models");
32
36
  const utils_1 = require("./utils");
37
+ const logger_1 = __importDefault(require("./logger"));
33
38
  function splitPaths(paths) {
34
39
  return (Array.isArray(paths) ? paths : [paths]).flatMap((p) => p.split(','));
35
40
  }
@@ -183,6 +188,18 @@ async function normalizeConfig(userConfig, mode) {
183
188
  return config;
184
189
  }
185
190
  async function normalizeClientConfig(client, mode) {
191
+ let publicPath = client.publicPath || path.normalize(`${client.publicPathPrefix || ''}/build/`);
192
+ if (client.moduleFederation) {
193
+ publicPath = path.normalize(`${publicPath}${client.moduleFederation.name}/`);
194
+ }
195
+ let transformCssWithLightningCss = Boolean(client.transformCssWithLightningCss);
196
+ if (client.moduleFederation?.isolateStyles && transformCssWithLightningCss) {
197
+ transformCssWithLightningCss = false;
198
+ logger_1.default.warning((0, common_tags_1.stripIndent) `
199
+ transformCssWithLightningCss option is disabled because moduleFederation.isolateStyles is enabled.
200
+ postcss loader will be used instead.
201
+ `);
202
+ }
186
203
  const normalizedConfig = {
187
204
  ...client,
188
205
  forkTsChecker: client.disableForkTsChecker ? false : client.forkTsChecker,
@@ -190,14 +207,18 @@ async function normalizeClientConfig(client, mode) {
190
207
  ? false
191
208
  : (client.reactRefresh ?? ((options) => options)),
192
209
  newJsxTransform: client.newJsxTransform ?? true,
193
- publicPath: client.publicPath || path.normalize(`${client.publicPathPrefix || ''}/build/`),
194
- assetsManifestFile: client.assetsManifestFile || 'assets-manifest.json',
210
+ publicPath,
211
+ assetsManifestFile: client.assetsManifestFile ||
212
+ (client.moduleFederation?.version
213
+ ? `assets-manifest-${client.moduleFederation.version}.json`
214
+ : 'assets-manifest.json'),
195
215
  modules: client.modules && remapPaths(client.modules),
196
216
  includes: client.includes && remapPaths(client.includes),
197
217
  images: client.images && remapPaths(client.images),
198
218
  hiddenSourceMap: client.hiddenSourceMap ?? true,
199
219
  svgr: client.svgr ?? {},
200
220
  entryFilter: client.entryFilter && splitPaths(client.entryFilter),
221
+ transformCssWithLightningCss,
201
222
  webpack: typeof client.webpack === 'function' ? client.webpack : (config) => config,
202
223
  rspack: typeof client.rspack === 'function' ? client.rspack : (config) => config,
203
224
  babel: typeof client.babel === 'function' ? client.babel : (config) => config,
@@ -16,6 +16,7 @@ import type { WebpackMode } from '../webpack/config';
16
16
  import type { UploadOptions } from '../s3-upload/upload';
17
17
  import type { TerserOptions } from 'terser-webpack-plugin';
18
18
  import type { ReactRefreshPluginOptions } from '@pmmmwh/react-refresh-webpack-plugin/types/lib/types';
19
+ import type { moduleFederationPlugin } from '@module-federation/enhanced';
19
20
  type Bundler = 'webpack' | 'rspack';
20
21
  type JavaScriptLoader = 'babel' | 'swc';
21
22
  type ServerCompiler = 'typescript' | 'swc';
@@ -59,6 +60,64 @@ interface LazyCompilationConfig {
59
60
  */
60
61
  entries?: boolean;
61
62
  }
63
+ export type ModuleFederationConfig = Omit<moduleFederationPlugin.ModuleFederationPluginOptions, 'name' | 'remotes'> & {
64
+ /**
65
+ * Unique name of the application in the Module Federation ecosystem
66
+ * Used as an identifier for this micro-frontend
67
+ */
68
+ name: string;
69
+ /**
70
+ * Application version, appended to the entry file name
71
+ * When specified, the file will be named `entry-{version}.js`
72
+ * @default undefined (file will be named `entry.js`)
73
+ */
74
+ version?: string;
75
+ /**
76
+ * Base URL for loading resources of this micro-frontend
77
+ * Should point to a publicly accessible URL where the files will be hosted
78
+ * @example 'https://cdn.example.com/my-app/'
79
+ */
80
+ publicPath: string;
81
+ /**
82
+ * List of remote application names that this application can load
83
+ * Simplified alternative to originalRemotes - only names are specified
84
+ * @example ['header', 'footer', 'navigation']
85
+ */
86
+ remotes?: string[];
87
+ /**
88
+ * Full configuration of remote applications in Module Federation format
89
+ * Allows more detailed configuration of each remote application
90
+ * @example { header: 'header@https://header.example.com/remoteEntry.js' }
91
+ */
92
+ originalRemotes?: moduleFederationPlugin.ModuleFederationPluginOptions['remotes'];
93
+ /**
94
+ * Enables runtime versioning for remote applications
95
+ * When enabled, remote applications will be loaded with version in the filename
96
+ * @default false
97
+ */
98
+ remotesRuntimeVersioning?: boolean;
99
+ /**
100
+ * CSS style isolation settings to prevent conflicts
101
+ * between styles of different micro-frontends
102
+ */
103
+ isolateStyles?: {
104
+ /**
105
+ * Function to generate CSS class prefix
106
+ * @param entryName - Application entry name
107
+ * @returns Prefix string for CSS classes
108
+ */
109
+ getPrefix: (entryName: string) => string;
110
+ /**
111
+ * Function to add prefix to CSS selectors
112
+ * @param prefix - Prefix to add
113
+ * @param selector - Original CSS selector
114
+ * @param prefixedSelector - Selector with added prefix
115
+ * @param filePath - Path to the styles file
116
+ * @returns Modified CSS selector
117
+ */
118
+ prefixSelector: (prefix: string, selector: string, prefixedSelector: string, filePath: string) => string;
119
+ };
120
+ };
62
121
  export interface ClientConfig {
63
122
  modules?: string[];
64
123
  /**
@@ -248,6 +307,11 @@ export interface ClientConfig {
248
307
  };
249
308
  bundler?: Bundler;
250
309
  javaScriptLoader?: JavaScriptLoader;
310
+ /**
311
+ * Module Federation configuration for building micro-frontends
312
+ * @see https://module-federation.io/
313
+ */
314
+ moduleFederation?: ModuleFederationConfig;
251
315
  }
252
316
  export interface CdnUploadConfig {
253
317
  bucket: string;
@@ -281,7 +345,7 @@ export interface ServiceConfig {
281
345
  verbose?: boolean;
282
346
  configPath?: string;
283
347
  }
284
- export type NormalizedClientConfig = Omit<ClientConfig, 'publicPathPrefix' | 'publicPath' | 'assetsManifestFile' | 'hiddenSourceMap' | 'svgr' | 'lazyCompilation' | 'devServer' | 'disableForkTsChecker' | 'disableReactRefresh'> & {
348
+ export type NormalizedClientConfig = Omit<ClientConfig, 'publicPathPrefix' | 'publicPath' | 'assetsManifestFile' | 'hiddenSourceMap' | 'svgr' | 'lazyCompilation' | 'devServer' | 'disableForkTsChecker' | 'disableReactRefresh' | 'transformCssWithLightningCss'> & {
285
349
  bundler: Bundler;
286
350
  javaScriptLoader: JavaScriptLoader;
287
351
  publicPath: string;
@@ -294,6 +358,7 @@ export type NormalizedClientConfig = Omit<ClientConfig, 'publicPathPrefix' | 'pu
294
358
  server?: ServerConfiguration;
295
359
  };
296
360
  verbose?: boolean;
361
+ transformCssWithLightningCss: boolean;
297
362
  webpack: (config: Configuration, options: {
298
363
  configType: `${WebpackMode}`;
299
364
  isSsr: boolean;
@@ -1,6 +1,30 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
26
  exports.createS3UploadPlugins = createS3UploadPlugins;
27
+ const path = __importStar(require("node:path"));
4
28
  const webpack_plugin_1 = require("./webpack-plugin");
5
29
  function createS3UploadPlugins(config, logger) {
6
30
  const plugins = [];
@@ -26,6 +50,10 @@ function createS3UploadPlugins(config, logger) {
26
50
  secretAccessKey,
27
51
  };
28
52
  }
53
+ let targetPath = cdn.prefix;
54
+ if (config.moduleFederation && targetPath !== undefined) {
55
+ targetPath = path.join(targetPath, config.moduleFederation.name);
56
+ }
29
57
  plugins.push(new webpack_plugin_1.S3UploadPlugin({
30
58
  exclude: config.hiddenSourceMap ? /\.map$/ : undefined,
31
59
  compress: cdn.compress,
@@ -36,7 +64,7 @@ function createS3UploadPlugins(config, logger) {
36
64
  },
37
65
  s3UploadOptions: {
38
66
  bucket: cdn.bucket,
39
- targetPath: cdn.prefix,
67
+ targetPath,
40
68
  cacheControl: cdn.cacheControl,
41
69
  },
42
70
  additionalPattern: cdn.additionalPattern,
@@ -1,4 +1,6 @@
1
- export declare function createRunFolder(): void;
1
+ import type { NormalizedServiceConfig } from './models';
2
+ export declare function getAppRunPath(config: NormalizedServiceConfig): string;
3
+ export declare function createRunFolder(config: NormalizedServiceConfig): void;
2
4
  export declare function shouldCompileTarget(target: 'client' | 'server' | undefined, targetName: string): boolean;
3
5
  export declare function getCacheDir(): Promise<string>;
4
6
  export declare function getPort({ port }: {
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getAppRunPath = getAppRunPath;
6
7
  exports.createRunFolder = createRunFolder;
7
8
  exports.shouldCompileTarget = shouldCompileTarget;
8
9
  exports.getCacheDir = getCacheDir;
@@ -10,11 +11,15 @@ exports.getPort = getPort;
10
11
  exports.deferredPromise = deferredPromise;
11
12
  const node_fs_1 = __importDefault(require("node:fs"));
12
13
  const node_os_1 = __importDefault(require("node:os"));
14
+ const node_path_1 = __importDefault(require("node:path"));
13
15
  const paths_1 = __importDefault(require("./paths"));
14
- function createRunFolder() {
15
- const runPath = paths_1.default.appRun;
16
- if (!node_fs_1.default.existsSync(runPath)) {
17
- node_fs_1.default.mkdirSync(runPath, { recursive: true });
16
+ function getAppRunPath(config) {
17
+ return node_path_1.default.resolve(paths_1.default.appRun, config.client.moduleFederation?.name || '');
18
+ }
19
+ function createRunFolder(config) {
20
+ const appRunPath = getAppRunPath(config);
21
+ if (!node_fs_1.default.existsSync(appRunPath)) {
22
+ node_fs_1.default.mkdirSync(appRunPath, { recursive: true });
18
23
  }
19
24
  }
20
25
  function shouldCompileTarget(target, targetName) {
@@ -9,8 +9,6 @@ export interface HelperOptions {
9
9
  isEnvProduction: boolean;
10
10
  configType: `${WebpackMode}`;
11
11
  buildDirectory: string;
12
- assetsManifestFile: string;
13
- entry?: string | string[] | Record<string, string | string[]>;
14
12
  entriesDirectory: string;
15
13
  isSsr: boolean;
16
14
  configPath?: string;
@@ -62,15 +62,17 @@ const fontSizeLimit = 8192;
62
62
  function getHelperOptions({ webpackMode, config, logger, isSsr = false, configPath, }) {
63
63
  const isEnvDevelopment = webpackMode === "development" /* WebpackMode.Dev */;
64
64
  const isEnvProduction = webpackMode === "production" /* WebpackMode.Prod */;
65
+ let buildDirectory = config.outputPath || (isSsr ? paths_1.default.appSsrBuild : paths_1.default.appBuild);
66
+ if (config.moduleFederation) {
67
+ buildDirectory = path.resolve(buildDirectory, config.moduleFederation.name);
68
+ }
65
69
  return {
66
70
  config,
67
71
  logger,
68
72
  isEnvDevelopment,
69
73
  isEnvProduction,
70
74
  configType: webpackMode,
71
- buildDirectory: config.outputPath || (isSsr ? paths_1.default.appSsrBuild : paths_1.default.appBuild),
72
- assetsManifestFile: config.assetsManifestFile,
73
- entry: config.entry,
75
+ buildDirectory,
74
76
  entriesDirectory: isSsr ? paths_1.default.appSsrEntry : paths_1.default.appEntry,
75
77
  isSsr,
76
78
  configPath,
@@ -91,13 +93,21 @@ function configureExternals({ config, isSsr }) {
91
93
  }
92
94
  function configureWebpackCache(options) {
93
95
  const { config } = options;
94
- if (typeof config.cache === 'object' && config.cache.type === 'filesystem') {
95
- return {
96
- ...config.cache,
96
+ let cache = config.cache;
97
+ if (typeof cache === 'object' && cache.type === 'filesystem') {
98
+ cache = {
99
+ ...cache,
97
100
  buildDependencies: getCacheBuildDependencies(options),
98
101
  };
102
+ if (config.moduleFederation) {
103
+ cache = {
104
+ name: config.moduleFederation.name,
105
+ version: config.moduleFederation.version,
106
+ ...cache,
107
+ };
108
+ }
99
109
  }
100
- return config.cache;
110
+ return cache;
101
111
  }
102
112
  async function webpackConfigFactory(options) {
103
113
  const { config } = options;
@@ -340,6 +350,10 @@ function configureResolve({ isEnvProduction, config }) {
340
350
  fallback: config.fallback,
341
351
  };
342
352
  }
353
+ function isModuleFederationEntry(entryName, fileName) {
354
+ // Ignore bootstrap file for module federation entries
355
+ return entryName === fileName || `${entryName}-bootstrap` === fileName;
356
+ }
343
357
  function createEntryArray(entry) {
344
358
  if (typeof entry === 'string') {
345
359
  return [require.resolve('./public-path'), entry];
@@ -352,24 +366,44 @@ function addEntry(entry, file) {
352
366
  [path.parse(file).name]: createEntryArray(file),
353
367
  };
354
368
  }
355
- function configureEntry({ config, entry, entriesDirectory }) {
356
- if (typeof entry === 'string' || Array.isArray(entry)) {
357
- return createEntryArray(entry);
369
+ function configureEntry({ config, entriesDirectory }) {
370
+ if (typeof config.entry === 'string' || Array.isArray(config.entry)) {
371
+ return createEntryArray(config.entry);
358
372
  }
359
- if (typeof entry === 'object') {
360
- return Object.entries(entry).reduce((acc, [key, value]) => ({
373
+ if (typeof config.entry === 'object') {
374
+ return Object.entries(config.entry).reduce((acc, [key, value]) => ({
361
375
  ...acc,
362
376
  [key]: createEntryArray(value),
363
377
  }), {});
364
378
  }
365
- let entries = fs.readdirSync(entriesDirectory).filter((file) => /\.[jt]sx?$/.test(file));
379
+ let entryFiles = fs.readdirSync(entriesDirectory).filter((file) => /\.[jt]sx?$/.test(file));
380
+ let result = {};
381
+ if (config.moduleFederation) {
382
+ const { name, remotes } = config.moduleFederation;
383
+ const entryFile = entryFiles.find((item) => path.parse(item).name === name);
384
+ if (!entryFile) {
385
+ throw new Error(`Entry "${name}" not found`);
386
+ }
387
+ // If remotes are not defined, it means that we are a remote
388
+ if (!remotes) {
389
+ return path.resolve(entriesDirectory, entryFile);
390
+ }
391
+ entryFiles = entryFiles.filter((file) => {
392
+ const fileName = path.parse(file).name;
393
+ return (!isModuleFederationEntry(name, fileName) &&
394
+ remotes.every((remote) => !isModuleFederationEntry(remote, fileName)));
395
+ });
396
+ result = {
397
+ main: createEntryArray(path.resolve(entriesDirectory, entryFile)),
398
+ };
399
+ }
366
400
  if (Array.isArray(config.entryFilter) && config.entryFilter.length) {
367
- entries = entries.filter((file) => config.entryFilter?.includes(file.split('.')[0] ?? ''));
401
+ entryFiles = entryFiles.filter((file) => config.entryFilter?.includes(path.parse(file).name));
368
402
  }
369
- if (!entries.length) {
403
+ if (!entryFiles.length) {
370
404
  throw new Error('No entries were found after applying entry filter');
371
405
  }
372
- return entries.reduce((acc, file) => addEntry(acc, path.resolve(entriesDirectory, file)), {});
406
+ return entryFiles.reduce((acc, file) => addEntry(acc, path.resolve(entriesDirectory, file)), result);
373
407
  }
374
408
  function getFileNames({ isEnvProduction, isSsr, config }) {
375
409
  let ext = 'js';
@@ -384,18 +418,25 @@ function getFileNames({ isEnvProduction, isSsr, config }) {
384
418
  };
385
419
  }
386
420
  function configureOutput(options) {
387
- let ssrOptions;
421
+ let ssrOptions, moduleFederationOptions;
388
422
  if (options.isSsr) {
389
423
  ssrOptions = {
390
424
  library: { type: options.config.ssr?.moduleType === 'esm' ? 'module' : 'commonjs2' },
391
425
  chunkFormat: false,
392
426
  };
393
427
  }
428
+ if (options.config.moduleFederation) {
429
+ moduleFederationOptions = {
430
+ publicPath: 'auto',
431
+ uniqueName: options.config.moduleFederation.name,
432
+ };
433
+ }
394
434
  return {
395
435
  ...getFileNames(options),
396
436
  path: options.buildDirectory,
397
437
  pathinfo: options.isEnvDevelopment,
398
438
  ...ssrOptions,
439
+ ...moduleFederationOptions,
399
440
  };
400
441
  }
401
442
  async function createJavaScriptLoader({ isEnvProduction, isEnvDevelopment, configType, config, isSsr, }) {
@@ -594,15 +635,27 @@ function getCssLoaders({ isEnvDevelopment, isEnvProduction, config, isSsr }, add
594
635
  const isRspack = config.bundler === 'rspack';
595
636
  const loaders = [];
596
637
  if (!config.transformCssWithLightningCss) {
638
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
639
+ const postcssPlugins = [
640
+ [require.resolve('postcss-preset-env'), { enableClientSidePolyfills: false }],
641
+ ];
642
+ if (config.moduleFederation?.isolateStyles) {
643
+ const { name, isolateStyles } = config.moduleFederation;
644
+ postcssPlugins.push([
645
+ require.resolve('postcss-prefix-selector'),
646
+ {
647
+ prefix: isolateStyles.getPrefix(name),
648
+ transform: isolateStyles.prefixSelector,
649
+ },
650
+ ]);
651
+ }
597
652
  loaders.push({
598
653
  loader: require.resolve('postcss-loader'),
599
654
  options: {
600
655
  sourceMap: !config.disableSourceMapGeneration,
601
656
  postcssOptions: {
602
657
  config: false,
603
- plugins: [
604
- [require.resolve('postcss-preset-env'), { enableClientSidePolyfills: false }],
605
- ],
658
+ plugins: postcssPlugins,
606
659
  },
607
660
  },
608
661
  });
@@ -908,6 +961,32 @@ function configureCommonPlugins(options, bundlerPlugins) {
908
961
  }));
909
962
  }
910
963
  plugins.push(createMomentTimezoneDataPlugin(config.momentTz));
964
+ if (config.moduleFederation) {
965
+ const { name, version, publicPath, remotes, originalRemotes, remotesRuntimeVersioning, runtimePlugins, ...restOptions } = config.moduleFederation;
966
+ let actualRemotes = originalRemotes;
967
+ if (remotes) {
968
+ actualRemotes = remotes.reduce((acc, remoteName) => {
969
+ const remoteFilename = remotesRuntimeVersioning
970
+ ? 'entry-[version].js'
971
+ : 'entry.js';
972
+ // eslint-disable-next-line no-param-reassign
973
+ acc[remoteName] =
974
+ `${remoteName}@${publicPath}${remoteName}/${remoteFilename}`;
975
+ return acc;
976
+ }, {});
977
+ }
978
+ const actualRuntimePlugins = runtimePlugins || [];
979
+ if (remotesRuntimeVersioning) {
980
+ actualRuntimePlugins.push(require.resolve('./runtime-versioning-plugin'));
981
+ }
982
+ plugins.push(new bundlerPlugins.ModuleFederationPlugin({
983
+ name,
984
+ filename: version ? `entry-${version}.js` : 'entry.js',
985
+ remotes: actualRemotes,
986
+ runtimePlugins: actualRuntimePlugins,
987
+ ...restOptions,
988
+ }));
989
+ }
911
990
  }
912
991
  if (isEnvProduction) {
913
992
  if (config.analyzeBundle === 'true') {
@@ -955,18 +1034,20 @@ function configureWebpackPlugins(options) {
955
1034
  TsCheckerPlugin: fork_ts_checker_webpack_plugin_1.default,
956
1035
  CSSExtractPlugin: mini_css_extract_plugin_1.default,
957
1036
  RSDoctorPlugin: require('@rsdoctor/webpack-plugin').RsdoctorWebpackPlugin,
1037
+ ModuleFederationPlugin: require('@module-federation/enhanced/webpack')
1038
+ .ModuleFederationPlugin,
958
1039
  };
959
1040
  const webpackPlugins = [
960
1041
  ...configureCommonPlugins(options, plugins),
961
1042
  new webpack_assets_manifest_1.default(isEnvProduction
962
1043
  ? {
963
1044
  entrypoints: true,
964
- output: options.assetsManifestFile,
1045
+ output: config.assetsManifestFile,
965
1046
  }
966
1047
  : {
967
1048
  entrypoints: true,
968
1049
  writeToDisk: true,
969
- output: path.resolve(options.buildDirectory, options.assetsManifestFile),
1050
+ output: path.resolve(options.buildDirectory, config.assetsManifestFile),
970
1051
  }),
971
1052
  ...(process.env.WEBPACK_PROFILE === 'true' ? [new webpack.debug.ProfilingPlugin()] : []),
972
1053
  ];
@@ -991,13 +1072,15 @@ function configureRspackPlugins(options) {
991
1072
  TsCheckerPlugin: ts_checker_rspack_plugin_1.TsCheckerRspackPlugin,
992
1073
  CSSExtractPlugin: core_1.rspack.CssExtractRspackPlugin,
993
1074
  RSDoctorPlugin: require('@rsdoctor/rspack-plugin').RsdoctorRspackPlugin,
1075
+ ModuleFederationPlugin: require('@module-federation/enhanced/rspack')
1076
+ .ModuleFederationPlugin,
994
1077
  };
995
1078
  const rspackPlugins = [
996
1079
  ...configureCommonPlugins(options, plugins),
997
1080
  new rspack_manifest_plugin_1.RspackManifestPlugin({
998
1081
  fileName: isEnvProduction
999
- ? options.assetsManifestFile
1000
- : path.resolve(options.buildDirectory, options.assetsManifestFile),
1082
+ ? config.assetsManifestFile
1083
+ : path.resolve(options.buildDirectory, config.assetsManifestFile),
1001
1084
  writeToFileEmit: true,
1002
1085
  useLegacyEmit: true,
1003
1086
  publicPath: '',
@@ -1183,7 +1266,7 @@ function configureRspackOptimization(helperOptions) {
1183
1266
  }
1184
1267
  const optimization = {
1185
1268
  splitChunks: getOptimizationSplitChunks(helperOptions),
1186
- runtimeChunk: 'single',
1269
+ runtimeChunk: helperOptions.config.moduleFederation ? false : 'single',
1187
1270
  minimizer: [new core_1.rspack.SwcJsMinimizerRspackPlugin(swcMinifyOptions), cssMinimizer],
1188
1271
  };
1189
1272
  return optimization;
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable camelcase, no-implicit-globals */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  // @ts-expect-error
4
5
  __webpack_public_path__ = globalThis.__PUBLIC_PATH__ ?? '/build/';
@@ -0,0 +1,5 @@
1
+ export default RuntimeVersioningPlugin;
2
+ declare function RuntimeVersioningPlugin(): {
3
+ name: string;
4
+ beforeRequest: (args: any) => any;
5
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /* global window */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const RuntimeVersioningPlugin = () => {
5
+ return {
6
+ name: 'runtime-versioning-plugin',
7
+ beforeRequest: (args) => {
8
+ if (typeof window.__REMOTE_VERSIONS__ !== 'object') {
9
+ return args;
10
+ }
11
+ args.options.remotes.forEach((remote) => {
12
+ const remoteVersion = window.__REMOTE_VERSIONS__[remote.name];
13
+ if (remoteVersion) {
14
+ // eslint-disable-next-line no-param-reassign
15
+ remote.entry = remote.entry.replace('[version]', remoteVersion);
16
+ }
17
+ });
18
+ return args;
19
+ },
20
+ };
21
+ };
22
+ exports.default = RuntimeVersioningPlugin;
@@ -113,8 +113,6 @@ async function configureWebpackConfigForStorybook(mode, userConfig = {}, storybo
113
113
  config: config.client,
114
114
  configType: mode,
115
115
  buildDirectory: config.client.outputPath || paths_1.default.appBuild,
116
- assetsManifestFile: config.client.assetsManifestFile,
117
- entry: config.client.entry,
118
116
  entriesDirectory: paths_1.default.appEntry,
119
117
  isSsr: false,
120
118
  };
@@ -1,3 +1,5 @@
1
1
  "use strict";
2
+ /* eslint-disable no-implicit-globals, camelcase */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ // @ts-expect-error
3
5
  __webpack_public_path__ = self.__PUBLIC_PATH__;
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export * from './common/s3-upload';
3
3
  export { createTransformPathsToLocalModules } from './common/typescript/transformers';
4
4
  export { defineConfig } from './common/models';
5
5
  export { babelPreset } from './common/babel';
6
- export type { ProjectConfig, ServiceConfig, LibraryConfig, ProjectFileConfig } from './common/models';
6
+ export type { ProjectConfig, ServiceConfig, LibraryConfig, ModuleFederationConfig, ProjectFileConfig, } from './common/models';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/app-builder",
3
- "version": "0.29.3",
3
+ "version": "0.30.1",
4
4
  "description": "Develop and build your React client-server projects, powered by typescript and webpack",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -68,6 +68,7 @@
68
68
  "@babel/preset-react": "^7.26.0",
69
69
  "@babel/preset-typescript": "^7.26.0",
70
70
  "@babel/runtime": "^7.26.0",
71
+ "@module-federation/enhanced": "^0.18.0",
71
72
  "@okikio/sharedworker": "^1.0.7",
72
73
  "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
73
74
  "@rsdoctor/rspack-plugin": "^1.0.2",
@@ -117,6 +118,7 @@
117
118
  "pino-pretty": "^11.2.0",
118
119
  "postcss": "^8.4.47",
119
120
  "postcss-loader": "^8.1.1",
121
+ "postcss-prefix-selector": "^2.1.1",
120
122
  "postcss-preset-env": "^9.1.3",
121
123
  "react": "^18.3.1",
122
124
  "react-dom": "^18.3.1",