@tramvai/module-child-app 2.76.2 → 2.77.0

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
@@ -62,7 +62,7 @@ This section will explain how the child-app are loaded and executed.
62
62
 
63
63
  ### DI
64
64
 
65
- Every child-app has its own DI-hierarchy which is isolated from other child app and partially from root-app. The only way communicate fpr DIs it's getting providers from root-app di inside child-app.
65
+ Every child-app has its own DI-hierarchy which is isolated from other child app and partially from root-app. The only way communicate for DIs it's getting providers from root-app DI inside child-app.
66
66
 
67
67
  Next picture shows connection between DI-containers in `root-app` and `child-app`s
68
68
 
@@ -80,6 +80,16 @@ How does it work when we trying to get provider from DI in `child-app`:
80
80
  1. If it exists then return it
81
81
  2. Throw error otherwise
82
82
 
83
+ There is a list of providers - exceptions, for which only factories will be borrowed, not instances, and new instances will be created in current Child-App DI:
84
+ - `COMMAND_LINE_RUNNER_TOKEN`
85
+ - `ACTION_EXECUTION_TOKEN`
86
+ - `ACTION_PAGE_RUNNER_TOKEN`
87
+ - `DISPATCHER_TOKEN`
88
+ - `STORE_TOKEN`
89
+ - `CONTEXT_TOKEN`
90
+ - `CREATE_CACHE_TOKEN`
91
+ - `CLEAR_CACHE_TOKEN`
92
+
83
93
  ### CommandLineRunner
84
94
 
85
95
  Each `child-app` has its own CommandLineRunner instance which allows to `child-app` make some preparations before the actual page render. This CommandLineRunner has almost identical lines as `root-app` to simplicity, but it is actually completely other line which are independent from lines in `root-app`
@@ -164,6 +174,39 @@ This token is considered undesirable to use as it leads to high coupling with st
164
174
 
165
175
  :::
166
176
 
177
+ ### Module Federation sharing dependencies
178
+
179
+ Child-apps utilizes [Module Federation](https://webpack.js.org/concepts/module-federation/) feature of webpack.
180
+
181
+ That allows child-apps:
182
+ - share dependencies between child-apps and root-app
183
+ - fallbacks to loading dependencies on request if implementation for dependency was not provided before or version of the dependency not satisfies request
184
+
185
+ The list of default shared dependencies is very short as it can increase bundle size in cases when child-apps are not used.
186
+
187
+ The following dependencies are shared by default:
188
+ - react core packages (react, react-dom, react/jsx-runtime)
189
+ - @tramvai/react
190
+ - @tinkoff/dippy
191
+ - @tramvai/core
192
+
193
+ To add additional dependency follow [instructions](#add-dependency-to-shared-list)
194
+
195
+ #### FAQ about shared dependencies
196
+
197
+ - **How shared dependencies look like?**. It mostly the implementation details but some info below might be useful for understanding:
198
+ - if shared dependency is provided in root-app the dependency will built in the initial chunk of root-app and dependency will be available without any additional network requests (these dependencies are marked as `eager` in moduleFederation config)
199
+ - if shared dependency is missing in the root-app then additional network request will be executed to some of child-app static files to load dependency code (the highest available version of dependency from all of child-apps will be loaded) i.e. additional js file with the name of shared dependency will be loaded on child-app usage.
200
+ - **How does shared dependencies affects root-app build?**. Using shared dependency slightly increases the generated bundle size. So it is preferred to make the list of shared dependencies as small as possible.
201
+ - **How versions of shared dependencies are resolved?**. Module federation will prefer to use the highest available version for the dependency but only if it satisfies the semver constraints of the all consumers. So it is preferred to use higher versions of the dependencies in the root-app and do not upgrade dependency versions in the child-apps without special need.
202
+ - **Dependency is added to list of shared but is not used by the app code**. Such dependency will not be provided and will not be available for consumption by other apps in that case.
203
+ - **How css is shared?**. Currently css are fully separated between root-app and child-app and child-app buid generates only single css file for the whole child-app
204
+ - **If two modules are using same shared dependency and root-app doesn't provide this dependency will the code for dep be loaded twice?**. It depends. On the client-side module federation will try to make only single network request, but with SSR it becomes a little more complicated and it is hard to resolve everything properly on server-side so sometimes it may lead to two network requests for different versions of the same dependency.
205
+ - **If version in child-app and root-app are not semver compatible**. Then child-app will load it's own version in that case as root-app cannot provide compatible version
206
+ - **Can I make sure the shared dependency is initialized only once across consumers?**. Yes, you can pass an object with `singleton` property instead of bare string in the tramvai.json config for shared dependency.
207
+ - **Should I add only high level wrapper of the dependencies I need to provide the list of all dependencies that I want to share?**. Better try different setups and see the output bundle size as it depends. The main rule is provide all of modules that might be imported by app code and that use the same low-level libraries. E.g. to share react-query integration add `@tramvai/module-react-query` and `@tramvai/react-query` to the shared dependencies
208
+ - **When building child-app I see two chunks related to the same package**. It happens due to some of caveats how module federation works. But anyway most of the time only single chunk will be used for the package, so just ignore the fact that in generated files you see two chunks.
209
+
167
210
  ### Error handling
168
211
 
169
212
  #### Error while loading child-app configs
@@ -359,6 +402,56 @@ const PageCmp: PageComponent = () => {
359
402
  PageCmp.childApps = [{ name: '[name]' }];
360
403
  ```
361
404
 
405
+ ### Add dependency to shared list
406
+
407
+ :::tip
408
+
409
+ To get most of the sharing dependencies add dependency both for root-apps that uses child-apps with the dependency and child-apps that uses the dependency
410
+
411
+ :::
412
+
413
+ In tramvai.json add new `shared` field
414
+
415
+ ```json
416
+ {
417
+ "projects": {
418
+ "root-app": {
419
+ "name": "root-app",
420
+ "root": "root-app",
421
+ "type": "application",
422
+ "hotRefresh": {
423
+ "enabled": true
424
+ },
425
+ "shared": {
426
+ "deps": [
427
+ "@tramvai/react-query",
428
+ "@tramvai/module-react-query",
429
+ { "name": "@tramvai/state", "singleton": true }
430
+ ]
431
+ }
432
+ },
433
+ "child-app": {
434
+ "name": "child-app",
435
+ "root": "child-app",
436
+ "type": "child-app",
437
+ "shared": {
438
+ "deps": [
439
+ "@tramvai/react-query",
440
+ "@tramvai/module-react-query",
441
+ { "name": "@tramvai/state", "singleton": true },
442
+ ]
443
+ }
444
+ }
445
+ }
446
+ }
447
+ ```
448
+
449
+ In order to choose what dependencies should be shared:
450
+ - use `tramvai analyze` command to explore the output bundle and how different options affects it
451
+ - try different dependencies and see what is loading on the page when child-app is used
452
+ - validate how adding shared dependency affects root-app bundle size through `trmavai analyze`
453
+
454
+
362
455
  ### Debug child-app problems
363
456
 
364
457
  If your are facing any problems while developing or using child-app use next instructions first.
@@ -441,3 +534,26 @@ Few advices to avoid this problem:
441
534
 
442
535
  - Memoize object, passed to child-app `props` property
443
536
  - Prevent pass to child-app properties, which can be changed during hydration, for example at client-side in page actions
537
+
538
+ ### Shared dependency are still loaded although the root-app shares it
539
+
540
+ Refer to the [FAQ](#faq-about-shared-dependencies) about the details. In summary:
541
+ - it is more reliable to provide shared dependency from the root-app than relying on sharing between several child-apps
542
+ - make sure all versions of the shared dependencies are semver compatible
543
+
544
+ ### Token with name already created!
545
+
546
+ The issue happens when `@tinkoff/dippy` library is shared due to fact that root-app and child-apps will have separate instances of the same tokens packages with the same naming.
547
+
548
+ For now, just ignore that kind of warnings during development. In producation these warnings won't be shown
549
+
550
+ ### Possible problems with shared dependency
551
+
552
+ #### react-query: No QueryClient set, use QueryClientProvider to set one
553
+
554
+ The issue may happen if there are different instances of `@tramvai/module-react-query` and `@tramvai/react-query` and therefore internal code inside `@tramvai/react-query` resolves React Context that differs from the QueryClient Provided inside `@tramvai/module-react-query`
555
+
556
+ To resolve the issue:
557
+ - when defining shared dependencies add both `@tramvai/module-react-query` and `@tramvai/module-react-query`
558
+ - make sure that both packages are used in the root-app (or none) as both instances should resolve to one place and if it isn't apply then for example `@tramvai/react-query` might instantiate with different React Context
559
+ - another option would be to add underlying library `@tanstack/react-query` to both child-app and root-app shared dependencies to make sure that required React Context is created only in single instance
@@ -2,10 +2,12 @@ import type { ChildApp } from '@tramvai/child-app-core';
2
2
  import type { ChildAppFinalConfig } from '@tramvai/tokens-child-app';
3
3
  import type { CREATE_CACHE_TOKEN, LOGGER_TOKEN } from '@tramvai/tokens-common';
4
4
  import { Loader } from '../shared/loader';
5
+ import type { ModuleFederationStats } from '../shared/webpack/moduleFederation';
5
6
  export declare class ServerLoader extends Loader {
6
7
  private readonly loader;
7
8
  private readonly initializedMap;
8
9
  private internalLoadCache;
10
+ private log;
9
11
  constructor({ logger, createCache, }: {
10
12
  logger: typeof LOGGER_TOKEN;
11
13
  createCache: typeof CREATE_CACHE_TOKEN;
@@ -13,4 +15,5 @@ export declare class ServerLoader extends Loader {
13
15
  load(config: ChildAppFinalConfig): Promise<ChildApp | void>;
14
16
  init(config: ChildAppFinalConfig): Promise<void>;
15
17
  get(config: ChildAppFinalConfig): ChildApp | void;
18
+ getStats(config: ChildAppFinalConfig): ModuleFederationStats | void;
16
19
  }
@@ -11,17 +11,28 @@ class ServerLoader extends Loader {
11
11
  max: 20,
12
12
  });
13
13
  this.internalLoadCache = cache;
14
+ this.log = logger('child-app:loader');
14
15
  this.loader = new ServerLoader$1({
15
16
  cache,
16
- log: logger('child-app:loader'),
17
+ log: this.log,
17
18
  });
18
19
  }
19
20
  async load(config) {
20
- await this.loader.resolveByUrl(config.server.entry, {
21
- codePrefix: `var ASSETS_PREFIX="${config.client.baseUrl}";`,
22
- displayName: config.name,
23
- kind: 'child-app',
24
- });
21
+ await Promise.all([
22
+ this.loader.resolveByUrl(config.server.entry, {
23
+ codePrefix: `var ASSETS_PREFIX="${config.client.baseUrl}";`,
24
+ displayName: config.name,
25
+ kind: 'child-app',
26
+ }),
27
+ this.loader
28
+ .resolveByUrl(config.client.stats, {
29
+ type: 'json',
30
+ kind: 'child-app stats',
31
+ displayName: config.name,
32
+ })
33
+ // we can live without stats
34
+ .catch(() => { }),
35
+ ]);
25
36
  await this.init(config);
26
37
  if (config.tag === 'debug') {
27
38
  setTimeout(() => {
@@ -32,16 +43,30 @@ class ServerLoader extends Loader {
32
43
  }
33
44
  async init(config) {
34
45
  const container = this.loader.getByUrl(config.server.entry);
35
- if (container) {
46
+ if (!container) {
47
+ return;
48
+ }
49
+ try {
36
50
  await initModuleFederation(container, 'default');
51
+ // copy some logic from https://github.com/module-federation/universe/blob/02221527aa684d2a37773c913bf341748fd34ecf/packages/node/src/plugins/loadScript.ts#L66
52
+ // to implement the same logic for loading child-app as UniversalModuleFederation
53
+ global.__remote_scope__._config[config.name] = config.server.entry;
37
54
  const factory = (await getModuleFederation(container, 'entry'));
38
55
  const entry = factory();
39
56
  this.initializedMap.set(container, entry);
40
57
  }
58
+ catch (err) {
59
+ this.log.error(err);
60
+ throw err;
61
+ }
41
62
  }
42
63
  get(config) {
43
64
  const container = this.loader.getByUrl(config.server.entry);
44
- return container && this.resolve(this.initializedMap.get(container));
65
+ const entry = container && this.initializedMap.get(container);
66
+ return entry && this.resolve(entry);
67
+ }
68
+ getStats(config) {
69
+ return this.loader.getByUrl(config.client.stats);
45
70
  }
46
71
  }
47
72
 
@@ -15,17 +15,28 @@ class ServerLoader extends loader.Loader {
15
15
  max: 20,
16
16
  });
17
17
  this.internalLoadCache = cache;
18
+ this.log = logger('child-app:loader');
18
19
  this.loader = new moduleLoaderServer.ServerLoader({
19
20
  cache,
20
- log: logger('child-app:loader'),
21
+ log: this.log,
21
22
  });
22
23
  }
23
24
  async load(config) {
24
- await this.loader.resolveByUrl(config.server.entry, {
25
- codePrefix: `var ASSETS_PREFIX="${config.client.baseUrl}";`,
26
- displayName: config.name,
27
- kind: 'child-app',
28
- });
25
+ await Promise.all([
26
+ this.loader.resolveByUrl(config.server.entry, {
27
+ codePrefix: `var ASSETS_PREFIX="${config.client.baseUrl}";`,
28
+ displayName: config.name,
29
+ kind: 'child-app',
30
+ }),
31
+ this.loader
32
+ .resolveByUrl(config.client.stats, {
33
+ type: 'json',
34
+ kind: 'child-app stats',
35
+ displayName: config.name,
36
+ })
37
+ // we can live without stats
38
+ .catch(() => { }),
39
+ ]);
29
40
  await this.init(config);
30
41
  if (config.tag === 'debug') {
31
42
  setTimeout(() => {
@@ -36,16 +47,30 @@ class ServerLoader extends loader.Loader {
36
47
  }
37
48
  async init(config) {
38
49
  const container = this.loader.getByUrl(config.server.entry);
39
- if (container) {
50
+ if (!container) {
51
+ return;
52
+ }
53
+ try {
40
54
  await moduleFederation.initModuleFederation(container, 'default');
55
+ // copy some logic from https://github.com/module-federation/universe/blob/02221527aa684d2a37773c913bf341748fd34ecf/packages/node/src/plugins/loadScript.ts#L66
56
+ // to implement the same logic for loading child-app as UniversalModuleFederation
57
+ global.__remote_scope__._config[config.name] = config.server.entry;
41
58
  const factory = (await moduleFederation.getModuleFederation(container, 'entry'));
42
59
  const entry = factory();
43
60
  this.initializedMap.set(container, entry);
44
61
  }
62
+ catch (err) {
63
+ this.log.error(err);
64
+ throw err;
65
+ }
45
66
  }
46
67
  get(config) {
47
68
  const container = this.loader.getByUrl(config.server.entry);
48
- return container && this.resolve(this.initializedMap.get(container));
69
+ const entry = container && this.initializedMap.get(container);
70
+ return entry && this.resolve(entry);
71
+ }
72
+ getStats(config) {
73
+ return this.loader.getByUrl(config.client.stats);
49
74
  }
50
75
  }
51
76
 
@@ -79,6 +79,7 @@ const serverProviders = [
79
79
  logger: LOGGER_TOKEN,
80
80
  diManager: CHILD_APP_DI_MANAGER_TOKEN,
81
81
  resolveFullConfig: CHILD_APP_RESOLVE_CONFIG_TOKEN,
82
+ loader: CHILD_APP_LOADER_TOKEN,
82
83
  preloadManager: CHILD_APP_PRELOAD_MANAGER_TOKEN,
83
84
  },
84
85
  }),
@@ -83,6 +83,7 @@ const serverProviders = [
83
83
  logger: tokensCommon.LOGGER_TOKEN,
84
84
  diManager: tokensChildApp.CHILD_APP_DI_MANAGER_TOKEN,
85
85
  resolveFullConfig: tokensChildApp.CHILD_APP_RESOLVE_CONFIG_TOKEN,
86
+ loader: tokensChildApp.CHILD_APP_LOADER_TOKEN,
86
87
  preloadManager: tokensChildApp.CHILD_APP_PRELOAD_MANAGER_TOKEN,
87
88
  },
88
89
  }),
@@ -1,9 +1,12 @@
1
1
  import type { ExtractDependencyType } from '@tinkoff/dippy';
2
- import type { ChildAppDiManager, ChildAppPreloadManager, CHILD_APP_RESOLVE_CONFIG_TOKEN } from '@tramvai/tokens-child-app';
2
+ import type { ChildAppDiManager, ChildAppLoader, ChildAppPreloadManager, CHILD_APP_RESOLVE_CONFIG_TOKEN } from '@tramvai/tokens-child-app';
3
3
  import type { LOGGER_TOKEN } from '@tramvai/tokens-common';
4
- export declare const registerChildAppRenderSlots: ({ logger, diManager, resolveFullConfig, preloadManager, }: {
4
+ import type { PageResource } from '@tramvai/tokens-render';
5
+ import type { ServerLoader } from './loader';
6
+ export declare const registerChildAppRenderSlots: ({ logger, diManager, resolveFullConfig, preloadManager, loader, }: {
5
7
  logger: ExtractDependencyType<typeof LOGGER_TOKEN>;
6
8
  diManager: ChildAppDiManager;
7
9
  resolveFullConfig: ExtractDependencyType<typeof CHILD_APP_RESOLVE_CONFIG_TOKEN>;
8
10
  preloadManager: ChildAppPreloadManager;
9
- }) => import("@tramvai/tokens-render").PageResource[];
11
+ loader: ChildAppLoader | ServerLoader;
12
+ }) => PageResource[];
@@ -1,32 +1,109 @@
1
- import { ResourceType, ResourceSlot, RENDER_SLOTS } from '@tramvai/tokens-render';
1
+ import { extname } from 'path';
2
+ import { gt, eq } from 'semver';
3
+ import flatten from '@tinkoff/utils/array/flatten';
4
+ import { resolve } from '@tinkoff/url';
5
+ import { RENDER_SLOTS, ResourceType, ResourceSlot } from '@tramvai/tokens-render';
6
+ import { getSharedScope } from '../shared/webpack/moduleFederation.es.js';
2
7
 
3
- const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, preloadManager, }) => {
8
+ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, preloadManager, loader, }) => {
4
9
  const log = logger('child-app:render:slots');
5
10
  const result = [];
6
- preloadManager.getPreloadedList().forEach((requestConfig) => {
7
- var _a;
11
+ const addChunk = (entry) => {
12
+ if (!entry) {
13
+ return;
14
+ }
15
+ const extension = extname(entry);
16
+ switch (extension) {
17
+ case '.js':
18
+ result.push({
19
+ type: ResourceType.script,
20
+ slot: ResourceSlot.HEAD_CORE_SCRIPTS,
21
+ payload: entry,
22
+ attrs: {
23
+ 'data-critical': 'true',
24
+ },
25
+ });
26
+ break;
27
+ case '.css':
28
+ result.push({
29
+ type: ResourceType.style,
30
+ slot: ResourceSlot.HEAD_CORE_STYLES,
31
+ payload: entry,
32
+ attrs: {
33
+ 'data-critical': 'true',
34
+ },
35
+ });
36
+ break;
37
+ }
38
+ };
39
+ const preloadedList = new Set(preloadManager.getPreloadedList());
40
+ const sharedScope = getSharedScope();
41
+ const mapSharedToChildApp = new Map();
42
+ // sharedScope will contain all of the shared chunks that were added
43
+ // while server is running
44
+ // but on the page we can use only shared chunks that either provided by the root-app
45
+ // or one of loaded child-app
46
+ // so gather all of the available shared modules, check the ones that are available in the currently
47
+ // preloaded child-apps and figure out the best single version of the dep
48
+ for (const shareKey in sharedScope) {
49
+ for (const version in sharedScope[shareKey]) {
50
+ const dep = sharedScope[shareKey][version];
51
+ const last = mapSharedToChildApp.get(shareKey);
52
+ const { eager, from } = dep;
53
+ const [type, name] = from.split(':');
54
+ if (!last ||
55
+ // module federation will pick the highest available version
56
+ // https://github.com/webpack/webpack/blob/b67626c7b4ffed8737d195b27c8cea1e68d58134/lib/sharing/ConsumeSharedRuntimeModule.js#L144
57
+ gt(version, last.version) ||
58
+ // if versions are equal then module federation will pick
59
+ // the dep with eager prop (it's set in root-app) of with the child-app with highest name in alphabetical order
60
+ (eq(version, last.version) && (eager !== last.eager ? eager : name > last.name))) {
61
+ mapSharedToChildApp.set(shareKey, { version, type, name, eager });
62
+ }
63
+ }
64
+ }
65
+ // eslint-disable-next-line max-statements
66
+ preloadedList.forEach((requestConfig) => {
67
+ var _a, _b, _c;
8
68
  const config = resolveFullConfig(requestConfig);
9
69
  if (!config) {
10
70
  return;
11
71
  }
72
+ const stats = 'getStats' in loader ? loader.getStats(config) : undefined;
12
73
  const di = diManager.getChildDi(config);
13
- result.push({
14
- type: ResourceType.script,
15
- slot: ResourceSlot.HEAD_CORE_SCRIPTS,
16
- payload: config.client.entry,
17
- attrs: {
18
- 'data-critical': 'true',
19
- },
20
- });
74
+ addChunk(config.client.entry);
21
75
  if (config.css) {
22
- result.push({
23
- type: ResourceType.style,
24
- slot: ResourceSlot.HEAD_CORE_STYLES,
25
- payload: (_a = config.css.entry) !== null && _a !== void 0 ? _a : null,
26
- attrs: {
27
- 'data-critical': 'true',
28
- },
29
- });
76
+ addChunk(config.css.entry);
77
+ }
78
+ if (stats) {
79
+ for (const federatedModule of stats.federatedModules) {
80
+ // entries are duplicated in the `exposes` field of federated stats for some reason
81
+ // for now there anyway should be only one exposed entry so took the first available
82
+ const files = new Set();
83
+ (_b = (_a = federatedModule === null || federatedModule === void 0 ? void 0 : federatedModule.exposes) === null || _a === void 0 ? void 0 : _a.entry) === null || _b === void 0 ? void 0 : _b.forEach((entry) => {
84
+ for (const key in entry) {
85
+ entry[key].forEach((file) => files.add(file));
86
+ }
87
+ });
88
+ for (const file of files) {
89
+ addChunk(resolve(config.client.baseUrl, file));
90
+ }
91
+ for (const sharedModule of federatedModule.sharedModules) {
92
+ const { shareKey } = (_c = sharedModule.provides) === null || _c === void 0 ? void 0 : _c[0];
93
+ const { chunks } = sharedModule;
94
+ const bestShared = mapSharedToChildApp.get(shareKey);
95
+ if (!(bestShared === null || bestShared === void 0 ? void 0 : bestShared.eager) && (bestShared === null || bestShared === void 0 ? void 0 : bestShared.name) === config.name) {
96
+ for (const chunk of chunks) {
97
+ addChunk(resolve(config.client.baseUrl, chunk));
98
+ }
99
+ // in stats.json federated stats could contain 2 sets of chunks for shared modules
100
+ // there usual one and fallback. For shared module there could be used any of this
101
+ // and the other one will be useless. So delete entry from map after its usage in order
102
+ // to add only single set of chunks for the same shared dep
103
+ mapSharedToChildApp.delete(shareKey);
104
+ }
105
+ }
106
+ }
30
107
  }
31
108
  if (!di) {
32
109
  return;
@@ -34,7 +111,7 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
34
111
  try {
35
112
  const renderSlots = di.get({ token: RENDER_SLOTS, optional: true });
36
113
  if (renderSlots) {
37
- result.push(...renderSlots);
114
+ result.push(...flatten(renderSlots));
38
115
  }
39
116
  }
40
117
  catch (error) {
@@ -2,35 +2,116 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var path = require('path');
6
+ var semver = require('semver');
7
+ var flatten = require('@tinkoff/utils/array/flatten');
8
+ var url = require('@tinkoff/url');
5
9
  var tokensRender = require('@tramvai/tokens-render');
10
+ var moduleFederation = require('../shared/webpack/moduleFederation.js');
6
11
 
7
- const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, preloadManager, }) => {
12
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
+
14
+ var flatten__default = /*#__PURE__*/_interopDefaultLegacy(flatten);
15
+
16
+ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, preloadManager, loader, }) => {
8
17
  const log = logger('child-app:render:slots');
9
18
  const result = [];
10
- preloadManager.getPreloadedList().forEach((requestConfig) => {
11
- var _a;
19
+ const addChunk = (entry) => {
20
+ if (!entry) {
21
+ return;
22
+ }
23
+ const extension = path.extname(entry);
24
+ switch (extension) {
25
+ case '.js':
26
+ result.push({
27
+ type: tokensRender.ResourceType.script,
28
+ slot: tokensRender.ResourceSlot.HEAD_CORE_SCRIPTS,
29
+ payload: entry,
30
+ attrs: {
31
+ 'data-critical': 'true',
32
+ },
33
+ });
34
+ break;
35
+ case '.css':
36
+ result.push({
37
+ type: tokensRender.ResourceType.style,
38
+ slot: tokensRender.ResourceSlot.HEAD_CORE_STYLES,
39
+ payload: entry,
40
+ attrs: {
41
+ 'data-critical': 'true',
42
+ },
43
+ });
44
+ break;
45
+ }
46
+ };
47
+ const preloadedList = new Set(preloadManager.getPreloadedList());
48
+ const sharedScope = moduleFederation.getSharedScope();
49
+ const mapSharedToChildApp = new Map();
50
+ // sharedScope will contain all of the shared chunks that were added
51
+ // while server is running
52
+ // but on the page we can use only shared chunks that either provided by the root-app
53
+ // or one of loaded child-app
54
+ // so gather all of the available shared modules, check the ones that are available in the currently
55
+ // preloaded child-apps and figure out the best single version of the dep
56
+ for (const shareKey in sharedScope) {
57
+ for (const version in sharedScope[shareKey]) {
58
+ const dep = sharedScope[shareKey][version];
59
+ const last = mapSharedToChildApp.get(shareKey);
60
+ const { eager, from } = dep;
61
+ const [type, name] = from.split(':');
62
+ if (!last ||
63
+ // module federation will pick the highest available version
64
+ // https://github.com/webpack/webpack/blob/b67626c7b4ffed8737d195b27c8cea1e68d58134/lib/sharing/ConsumeSharedRuntimeModule.js#L144
65
+ semver.gt(version, last.version) ||
66
+ // if versions are equal then module federation will pick
67
+ // the dep with eager prop (it's set in root-app) of with the child-app with highest name in alphabetical order
68
+ (semver.eq(version, last.version) && (eager !== last.eager ? eager : name > last.name))) {
69
+ mapSharedToChildApp.set(shareKey, { version, type, name, eager });
70
+ }
71
+ }
72
+ }
73
+ // eslint-disable-next-line max-statements
74
+ preloadedList.forEach((requestConfig) => {
75
+ var _a, _b, _c;
12
76
  const config = resolveFullConfig(requestConfig);
13
77
  if (!config) {
14
78
  return;
15
79
  }
80
+ const stats = 'getStats' in loader ? loader.getStats(config) : undefined;
16
81
  const di = diManager.getChildDi(config);
17
- result.push({
18
- type: tokensRender.ResourceType.script,
19
- slot: tokensRender.ResourceSlot.HEAD_CORE_SCRIPTS,
20
- payload: config.client.entry,
21
- attrs: {
22
- 'data-critical': 'true',
23
- },
24
- });
82
+ addChunk(config.client.entry);
25
83
  if (config.css) {
26
- result.push({
27
- type: tokensRender.ResourceType.style,
28
- slot: tokensRender.ResourceSlot.HEAD_CORE_STYLES,
29
- payload: (_a = config.css.entry) !== null && _a !== void 0 ? _a : null,
30
- attrs: {
31
- 'data-critical': 'true',
32
- },
33
- });
84
+ addChunk(config.css.entry);
85
+ }
86
+ if (stats) {
87
+ for (const federatedModule of stats.federatedModules) {
88
+ // entries are duplicated in the `exposes` field of federated stats for some reason
89
+ // for now there anyway should be only one exposed entry so took the first available
90
+ const files = new Set();
91
+ (_b = (_a = federatedModule === null || federatedModule === void 0 ? void 0 : federatedModule.exposes) === null || _a === void 0 ? void 0 : _a.entry) === null || _b === void 0 ? void 0 : _b.forEach((entry) => {
92
+ for (const key in entry) {
93
+ entry[key].forEach((file) => files.add(file));
94
+ }
95
+ });
96
+ for (const file of files) {
97
+ addChunk(url.resolve(config.client.baseUrl, file));
98
+ }
99
+ for (const sharedModule of federatedModule.sharedModules) {
100
+ const { shareKey } = (_c = sharedModule.provides) === null || _c === void 0 ? void 0 : _c[0];
101
+ const { chunks } = sharedModule;
102
+ const bestShared = mapSharedToChildApp.get(shareKey);
103
+ if (!(bestShared === null || bestShared === void 0 ? void 0 : bestShared.eager) && (bestShared === null || bestShared === void 0 ? void 0 : bestShared.name) === config.name) {
104
+ for (const chunk of chunks) {
105
+ addChunk(url.resolve(config.client.baseUrl, chunk));
106
+ }
107
+ // in stats.json federated stats could contain 2 sets of chunks for shared modules
108
+ // there usual one and fallback. For shared module there could be used any of this
109
+ // and the other one will be useless. So delete entry from map after its usage in order
110
+ // to add only single set of chunks for the same shared dep
111
+ mapSharedToChildApp.delete(shareKey);
112
+ }
113
+ }
114
+ }
34
115
  }
35
116
  if (!di) {
36
117
  return;
@@ -38,7 +119,7 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
38
119
  try {
39
120
  const renderSlots = di.get({ token: tokensRender.RENDER_SLOTS, optional: true });
40
121
  if (renderSlots) {
41
- result.push(...renderSlots);
122
+ result.push(...flatten__default["default"](renderSlots));
42
123
  }
43
124
  }
44
125
  catch (error) {
@@ -80,6 +80,7 @@ const sharedProviders = [
80
80
  client: {
81
81
  baseUrl: `${baseUrl}${name}/`,
82
82
  entry: `${baseUrl}${name}/${name}_client@${version}.js`,
83
+ stats: `${baseUrl}${name}/${name}_stats@${version}.json`,
83
84
  ...client,
84
85
  },
85
86
  css: withoutCss
@@ -80,6 +80,7 @@ const sharedProviders = [
80
80
  client: {
81
81
  baseUrl: `${baseUrl}${name}/`,
82
82
  entry: `${baseUrl}${name}/${name}_client@${version}.js`,
83
+ stats: `${baseUrl}${name}/${name}_stats@${version}.json`,
83
84
  ...client,
84
85
  },
85
86
  css: withoutCss
@@ -84,6 +84,7 @@ const sharedProviders = [
84
84
  client: {
85
85
  baseUrl: `${baseUrl}${name}/`,
86
86
  entry: `${baseUrl}${name}/${name}_client@${version}.js`,
87
+ stats: `${baseUrl}${name}/${name}_stats@${version}.json`,
87
88
  ...client,
88
89
  },
89
90
  css: withoutCss
@@ -3,16 +3,21 @@ const initModuleFederation = async (container, scope = 'default') => {
3
3
  await container.init(__webpack_share_scopes__[scope]);
4
4
  return;
5
5
  }
6
+ if (typeof window === 'undefined') {
7
+ // copy some logic from https://github.com/module-federation/universe/blob/02221527aa684d2a37773c913bf341748fd34ecf/packages/node/src/plugins/loadScript.ts#L66
8
+ // to implement the same logic for loading child-app as UniversalModuleFederation
9
+ global.__remote_scope__ = global.__remote_scope__ || { _config: {} };
10
+ }
6
11
  await __webpack_init_sharing__('default');
7
- // currently module federation has problems with external modules
8
- // and unfourtanelly react and react-dom are marked as externals in defaults
12
+ // currently module federation has problems with external modules (they are marked as externals in the dev build)
13
+ // and unfortunately react and react-dom are marked as externals in defaults
9
14
  // fill sharedScope manually here
10
15
  const shareScope = __webpack_share_scopes__[scope];
11
16
  if (!shareScope.react) {
12
17
  shareScope.react = {
13
18
  '*': {
14
19
  get: () => () => require('react'),
15
- from: 'tramvai-mf-fix',
20
+ from: 'application:tramvai-mf-fix',
16
21
  eager: true,
17
22
  loaded: true,
18
23
  },
@@ -22,7 +27,7 @@ const initModuleFederation = async (container, scope = 'default') => {
22
27
  shareScope['react-dom'] = {
23
28
  '*': {
24
29
  get: () => () => require('react-dom'),
25
- from: 'tramvai-mf-fix',
30
+ from: 'application:tramvai-mf-fix',
26
31
  eager: true,
27
32
  loaded: true,
28
33
  },
@@ -34,7 +39,7 @@ const initModuleFederation = async (container, scope = 'default') => {
34
39
  shareScope['react/jsx-runtime'] = {
35
40
  '*': {
36
41
  get: () => () => require('react/jsx-runtime'),
37
- from: 'tramvai-mf-fix',
42
+ from: 'application:tramvai-mf-fix',
38
43
  eager: true,
39
44
  loaded: true,
40
45
  },
@@ -1,15 +1,48 @@
1
1
  declare global {
2
2
  var __webpack_init_sharing__: (name: string) => Promise<void>;
3
- var __webpack_share_scopes__: ModuleFederationSharedScope;
3
+ var __webpack_share_scopes__: ModuleFederationSharedScopes;
4
+ var __remote_scope__: {
5
+ _config: Record<string, string>;
6
+ };
4
7
  }
5
8
  interface ModuleFederationSharedScope {
6
- default: Record<string, any>;
7
- [index: string]: Record<string, any>;
9
+ [packageName: string]: {
10
+ [version: string]: {
11
+ get: Function;
12
+ from: string;
13
+ eager: boolean;
14
+ loaded: 0 | 1 | boolean;
15
+ };
16
+ };
17
+ }
18
+ interface ModuleFederationSharedScopes {
19
+ default: ModuleFederationSharedScope;
20
+ [scope: string]: ModuleFederationSharedScope;
8
21
  }
9
22
  export interface ModuleFederationContainer {
10
- init(scope: ModuleFederationSharedScope[string]): Promise<void>;
23
+ init(scope: ModuleFederationSharedScopes[string]): Promise<void>;
11
24
  get<T = unknown>(name: string): Promise<T>;
12
25
  }
26
+ export interface ModuleFederationStats {
27
+ sharedModules: any[];
28
+ federatedModules: Array<{
29
+ remote: string;
30
+ entry: string;
31
+ sharedModules: Array<{
32
+ chunks: string[];
33
+ provides: Array<{
34
+ shareScope: string;
35
+ shareKey: string;
36
+ requiredVersion: string;
37
+ strictVersion: boolean;
38
+ singleton: boolean;
39
+ eager: boolean;
40
+ }>;
41
+ }>;
42
+ exposes: Record<string, Array<Record<string, string[]>>>;
43
+ }>;
44
+ }
45
+ export declare const getSharedScope: (scope?: string) => ModuleFederationSharedScope;
13
46
  export declare const initModuleFederation: (container?: ModuleFederationContainer, scope?: string) => Promise<void>;
14
47
  export declare const getModuleFederation: (container: ModuleFederationContainer, name?: string) => Promise<unknown>;
15
48
  export {};
@@ -1,18 +1,26 @@
1
+ const getSharedScope = (scope = 'default') => {
2
+ return __webpack_share_scopes__[scope];
3
+ };
1
4
  const initModuleFederation = async (container, scope = 'default') => {
2
5
  if (container) {
3
6
  await container.init(__webpack_share_scopes__[scope]);
4
7
  return;
5
8
  }
9
+ if (typeof window === 'undefined') {
10
+ // copy some logic from https://github.com/module-federation/universe/blob/02221527aa684d2a37773c913bf341748fd34ecf/packages/node/src/plugins/loadScript.ts#L66
11
+ // to implement the same logic for loading child-app as UniversalModuleFederation
12
+ global.__remote_scope__ = global.__remote_scope__ || { _config: {} };
13
+ }
6
14
  await __webpack_init_sharing__('default');
7
- // currently module federation has problems with external modules
8
- // and unfourtanelly react and react-dom are marked as externals in defaults
15
+ // currently module federation has problems with external modules (they are marked as externals in the dev build)
16
+ // and unfortunately react and react-dom are marked as externals in defaults
9
17
  // fill sharedScope manually here
10
18
  const shareScope = __webpack_share_scopes__[scope];
11
19
  if (!shareScope.react) {
12
20
  shareScope.react = {
13
21
  '*': {
14
22
  get: () => () => require('react'),
15
- from: 'tramvai-mf-fix',
23
+ from: 'application:tramvai-mf-fix',
16
24
  eager: true,
17
25
  loaded: true,
18
26
  },
@@ -22,7 +30,7 @@ const initModuleFederation = async (container, scope = 'default') => {
22
30
  shareScope['react-dom'] = {
23
31
  '*': {
24
32
  get: () => () => require('react-dom'),
25
- from: 'tramvai-mf-fix',
33
+ from: 'application:tramvai-mf-fix',
26
34
  eager: true,
27
35
  loaded: true,
28
36
  },
@@ -34,7 +42,7 @@ const initModuleFederation = async (container, scope = 'default') => {
34
42
  shareScope['react/jsx-runtime'] = {
35
43
  '*': {
36
44
  get: () => () => require('react/jsx-runtime'),
37
- from: 'tramvai-mf-fix',
45
+ from: 'application:tramvai-mf-fix',
38
46
  eager: true,
39
47
  loaded: true,
40
48
  },
@@ -46,4 +54,4 @@ const getModuleFederation = async (container, name = 'entry') => {
46
54
  return container.get(name);
47
55
  };
48
56
 
49
- export { getModuleFederation, initModuleFederation };
57
+ export { getModuleFederation, getSharedScope, initModuleFederation };
@@ -2,21 +2,29 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ const getSharedScope = (scope = 'default') => {
6
+ return __webpack_share_scopes__[scope];
7
+ };
5
8
  const initModuleFederation = async (container, scope = 'default') => {
6
9
  if (container) {
7
10
  await container.init(__webpack_share_scopes__[scope]);
8
11
  return;
9
12
  }
13
+ if (typeof window === 'undefined') {
14
+ // copy some logic from https://github.com/module-federation/universe/blob/02221527aa684d2a37773c913bf341748fd34ecf/packages/node/src/plugins/loadScript.ts#L66
15
+ // to implement the same logic for loading child-app as UniversalModuleFederation
16
+ global.__remote_scope__ = global.__remote_scope__ || { _config: {} };
17
+ }
10
18
  await __webpack_init_sharing__('default');
11
- // currently module federation has problems with external modules
12
- // and unfourtanelly react and react-dom are marked as externals in defaults
19
+ // currently module federation has problems with external modules (they are marked as externals in the dev build)
20
+ // and unfortunately react and react-dom are marked as externals in defaults
13
21
  // fill sharedScope manually here
14
22
  const shareScope = __webpack_share_scopes__[scope];
15
23
  if (!shareScope.react) {
16
24
  shareScope.react = {
17
25
  '*': {
18
26
  get: () => () => require('react'),
19
- from: 'tramvai-mf-fix',
27
+ from: 'application:tramvai-mf-fix',
20
28
  eager: true,
21
29
  loaded: true,
22
30
  },
@@ -26,7 +34,7 @@ const initModuleFederation = async (container, scope = 'default') => {
26
34
  shareScope['react-dom'] = {
27
35
  '*': {
28
36
  get: () => () => require('react-dom'),
29
- from: 'tramvai-mf-fix',
37
+ from: 'application:tramvai-mf-fix',
30
38
  eager: true,
31
39
  loaded: true,
32
40
  },
@@ -38,7 +46,7 @@ const initModuleFederation = async (container, scope = 'default') => {
38
46
  shareScope['react/jsx-runtime'] = {
39
47
  '*': {
40
48
  get: () => () => require('react/jsx-runtime'),
41
- from: 'tramvai-mf-fix',
49
+ from: 'application:tramvai-mf-fix',
42
50
  eager: true,
43
51
  loaded: true,
44
52
  },
@@ -51,4 +59,5 @@ const getModuleFederation = async (container, name = 'entry') => {
51
59
  };
52
60
 
53
61
  exports.getModuleFederation = getModuleFederation;
62
+ exports.getSharedScope = getSharedScope;
54
63
  exports.initModuleFederation = initModuleFederation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-child-app",
3
- "version": "2.76.2",
3
+ "version": "2.77.0",
4
4
  "description": "Module for child apps",
5
5
  "browser": {
6
6
  "./lib/server.js": "./lib/browser.js",
@@ -29,25 +29,27 @@
29
29
  "dependencies": {
30
30
  "@tinkoff/env-validators": "0.1.5",
31
31
  "@tinkoff/module-loader-client": "0.4.5",
32
- "@tinkoff/module-loader-server": "0.5.7",
33
- "@tramvai/child-app-core": "2.76.2",
34
- "@tramvai/module-router": "2.76.2",
32
+ "@tinkoff/module-loader-server": "0.5.8",
33
+ "@tinkoff/url": "0.8.5",
34
+ "@tramvai/child-app-core": "2.77.0",
35
+ "@tramvai/module-router": "2.77.0",
35
36
  "@tramvai/safe-strings": "0.5.7",
36
- "@tramvai/tokens-child-app": "2.76.2"
37
+ "@tramvai/tokens-child-app": "2.77.0"
37
38
  },
38
39
  "devDependencies": {},
39
40
  "peerDependencies": {
40
41
  "@tinkoff/dippy": "0.8.13",
41
42
  "@tinkoff/utils": "^2.1.2",
42
- "@tramvai/core": "2.76.2",
43
- "@tramvai/state": "2.76.2",
44
- "@tramvai/react": "2.76.2",
45
- "@tramvai/tokens-common": "2.76.2",
46
- "@tramvai/tokens-render": "2.76.2",
47
- "@tramvai/tokens-router": "2.76.2",
43
+ "@tramvai/core": "2.77.0",
44
+ "@tramvai/state": "2.77.0",
45
+ "@tramvai/react": "2.77.0",
46
+ "@tramvai/tokens-common": "2.77.0",
47
+ "@tramvai/tokens-render": "2.77.0",
48
+ "@tramvai/tokens-router": "2.77.0",
48
49
  "react": ">=16.14.0",
49
50
  "react-dom": ">=16.14.0",
50
51
  "object-assign": "^4.1.1",
52
+ "semver": "^7.3.8",
51
53
  "tslib": "^2.4.0"
52
54
  },
53
55
  "module": "lib/server.es.js"