@tramvai/module-child-app 2.61.1 → 2.63.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
@@ -32,6 +32,34 @@ createApp({
32
32
  - `RequestDI` - DI-Container which is created for every request and represents specific data for single client. RequestDI inherits providers from SingletonDI and it is independent from other RequestDIs
33
33
  - `CommandLineRunner` - instance of [CommandModule](references/modules/common.md#commandmodule)
34
34
 
35
+ ### Workflow
36
+
37
+ This section will explain how the child-app are loaded and executed.
38
+
39
+ #### Both SSR + Client hydration
40
+
41
+ 1. Provider `CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN` will assemble all of the configs for child-apps that were provided through token `CHILD_APP_RESOLUTION_CONFIGS_TOKEN` and resolve the configs on `resolvePageDeps` command line
42
+ 2. Provider `CHILD_APP_RESOLVE_CONFIG_TOKEN` is used to generate config that are later consumable by child-app loader
43
+ 3. Child-apps that will be rendered on the page should be preloaded with `CHILD_APP_PRELOAD_MANAGER_TOKEN` - see [preload child-app](#preload-child-app)
44
+
45
+ #### SSR
46
+
47
+ 1. For every child-app that was preloaded server loads its code and executes all of the initialization - see [loading child-app](#loading-child-app)
48
+ 2. Any child-app that were preloaded during request are added as script tag to client code to the output html
49
+ 3. During render for child-apps their render code is executed to provide proper HTML
50
+ 4. State is dehydrated for child-app the same time as root-app's state
51
+
52
+ #### Client hydration
53
+
54
+ 1. For every child-app that was preloaded on server tramvai executes all of the initialization - see [loading child-app](#loading-child-app). In other cases initialization happens during first usage
55
+ 2. If child-app was preloaded on server than client code should be loaded on page loaded. Otherwise tramvai will try to load client code on preload call on client side or during attempt to render child-app
56
+ 3. During page render react will attempt to rehydrate render for child-apps that came from server. In case of errors it will rerender it from scratch
57
+
58
+ #### SPA navigations
59
+
60
+ 1. During loading for next route child-app might be preloaded - it will be initialized during loading in that case otherwise child-app will be loaded as soon as it will be used.
61
+ 2. While loading child-app it will render null. After loading child-app's render function will be used
62
+
35
63
  ### DI
36
64
 
37
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.
@@ -136,8 +164,70 @@ This token is considered undesirable to use as it leads to high coupling with st
136
164
 
137
165
  :::
138
166
 
167
+ ### Error handling
168
+
169
+ #### Error while loading child-app configs
170
+
171
+ Child-app configs might be loaded with providers for multi token `CHILD_APP_RESOLUTION_CONFIGS_TOKEN` that are implemented in custom modules or in the app code.
172
+
173
+ Error that were raised in custom providers will be logged as errors under `child-app:resolution-config` key. After that there errors will be ignored and won't affect other resolutions, but the configs that could be loaded with that provider will be lost.
174
+
175
+ #### Child-app with specified name was not found
176
+
177
+ There is 2 causes that may lead to missing child-app in config:
178
+
179
+ - configs defined through `CHILD_APP_RESOLUTION_CONFIGS_TOKEN` was failed and therefore there is no info about used child-app
180
+ - wrong naming of child-app
181
+
182
+ In any of that causes the error about missing child-app will be logged and the render for it will just return null.
183
+
184
+ If you are facing that problem first check the logs about errors for loading child-app configs than check that naming is right and such child-app exists in your configs.
185
+
186
+ #### Failed to load child-app code
187
+
188
+ Request to child-app code can fail by various causes.
189
+
190
+ If request has failed on server side the script tag with link to child-app client code will still be added to the html in order to try to load the child-app on client side. It will render fallback if provided or null on SSR (wrapped in Suspense for react@18) in that case and will try to resolve and render the child-app on the client.
191
+
192
+ If request has failed on client side it will render [fallback](#fallback) passing error or the default errorBoundary component.
193
+
194
+ #### Error during child-app render
195
+
196
+ Errors that happens inside child-app's render function
197
+
198
+ If render has failed on server side it will render fallback if provided or null otherwise. It may then proper rehydrated on client side.
199
+
200
+ If render has failed on client side it will render fallback with error if provided or default errorBoundary component
201
+
202
+ #### Error in commandLine handler
203
+
204
+ Any errors inside child-app commandLine execution will be logged and won't affect the execution of the root-app.
205
+
139
206
  ## API
140
207
 
208
+ ### ChildApp
209
+
210
+ React component to render child-app with specified config in the react tree
211
+
212
+ ```ts
213
+ import React from 'react';
214
+ import { ChildApp } from '@tramvai/module-child-app';
215
+
216
+ export const Page = () => {
217
+ return (
218
+ <div>
219
+ ...
220
+ <ChildApp name="[name]" />
221
+ ...
222
+ </div>
223
+ );
224
+ };
225
+ ```
226
+
227
+ #### fallback
228
+
229
+ React.ComponentType that will be rendered while child-app is loading (by default is null) or there was an error inside child-app (by default is a standard errorBoundary component)
230
+
141
231
  ### CHILD_APP_INTERNAL_ROOT_STATE_ALLOWED_STORE_TOKEN
142
232
 
143
233
  Defines the list of allowed root-app store names that might be used inside child-app.
@@ -269,7 +359,14 @@ const PageCmp: PageComponent = () => {
269
359
  PageCmp.childApps = [{ name: '[name]' }];
270
360
  ```
271
361
 
272
- ### Debug child-app
362
+ ### Debug child-app problems
363
+
364
+ If your are facing any problems while developing or using child-app use next instructions first.
365
+
366
+ 1. Check the logs with key `child-app` that may lead to source of problems
367
+ 2. If there is not enough logs enable all `child-app` logs - [how to display logs](references/modules/log.md#display-logs)
368
+
369
+ ### Run debug version of child-app
273
370
 
274
371
  #### Single child-app
275
372
 
@@ -328,13 +425,11 @@ You may specify a full config to debug to a specific child-app:
328
425
 
329
426
  ### This Suspense boundary received an update before it finished hydrating
330
427
 
331
- When `React` >= `18` version is used, child-app will be wrapped in `Suspense` boundary for [Selective Hydration](https://github.com/reactwg/react-18/discussions/130).
332
- This optimization can significantly decrease Total Blocking Time metric of the page.
428
+ When `React` >= `18` version is used, child-app will be wrapped in `Suspense` boundary for [Selective Hydration](https://github.com/reactwg/react-18/discussions/130). This optimization can significantly decrease Total Blocking Time metric of the page.
333
429
 
334
- There is one drawback of this optimization - if you will try rerender child-app during selective hydration, `React` will switch to deopt mode and made full client-rendering of the child-app component.
335
- Potential ways to fix this problem [described here](https://github.com/facebook/react/issues/24476#issuecomment-1127800350).
336
- `ChildApp` component already wrapped in `React.memo`.
430
+ There is one drawback of this optimization - if you will try rerender child-app during selective hydration, `React` will switch to deopt mode and made full client-rendering of the child-app component. Potential ways to fix this problem [described here](https://github.com/facebook/react/issues/24476#issuecomment-1127800350). `ChildApp` component already wrapped in `React.memo`.
337
431
 
338
432
  Few advices to avoid this problem:
433
+
339
434
  - Memoize object, passed to child-app `props` property
340
- - Prevent pass to child-app properties, which can be changed during hydration, for example at cliend-side in page actions
435
+ - Prevent pass to child-app properties, which can be changed during hydration, for example at client-side in page actions
@@ -11,6 +11,7 @@ export declare class PreloadManager implements ChildAppPreloadManager {
11
11
  private currentlyPreloaded;
12
12
  private hasPreloadBefore;
13
13
  private hasInitialized;
14
+ private map;
14
15
  constructor({ loader, runner, resolutionConfigManager, resolveExternalConfig, store, }: {
15
16
  loader: ChildAppLoader;
16
17
  runner: ChildAppCommandLineRunner;
@@ -12,7 +12,6 @@ export declare class RenderManager implements ChildAppRenderManager {
12
12
  diManager: ChildAppDiManager;
13
13
  resolveExternalConfig: typeof CHILD_APP_RESOLVE_CONFIG_TOKEN;
14
14
  });
15
- getChildDi(request: ChildAppRequestConfig): [Container | null, null | Promise<Container | null>];
16
- flush(): Promise<boolean>;
15
+ getChildDi(request: ChildAppRequestConfig): [Container | undefined, undefined | Promise<Container | undefined>];
17
16
  clear(): void;
18
17
  }
@@ -7,14 +7,12 @@ export declare class RenderManager implements ChildAppRenderManager {
7
7
  private readonly resolveFullConfig;
8
8
  private readonly log;
9
9
  private readonly hasRenderedSet;
10
- private readonly loadingInProgress;
11
10
  constructor({ logger, preloadManager, diManager, resolveFullConfig, }: {
12
11
  logger: typeof LOGGER_TOKEN;
13
12
  preloadManager: ChildAppPreloadManager;
14
13
  diManager: ChildAppDiManager;
15
14
  resolveFullConfig: typeof CHILD_APP_RESOLVE_CONFIG_TOKEN;
16
15
  });
17
- getChildDi(request: ChildAppRequestConfig): [Container | null, null | Promise<Container | null>];
18
- flush(): Promise<boolean>;
16
+ getChildDi(request: ChildAppRequestConfig): [Container | undefined, undefined | Promise<Container | undefined>];
19
17
  clear(): void;
20
18
  }
@@ -6,22 +6,23 @@ export * from '@tramvai/tokens-child-app';
6
6
  import { CONTEXT_TOKEN, ACTION_PAGE_RUNNER_TOKEN, LOGGER_TOKEN, DISPATCHER_TOKEN, STORE_TOKEN, DISPATCHER_CONTEXT_TOKEN, STORE_MIDDLEWARE, INITIAL_APP_STATE_TOKEN, COMBINE_REDUCERS, ENV_MANAGER_TOKEN, REGISTER_CLEAR_CACHE_TOKEN, CLEAR_CACHE_TOKEN, ENV_USED_TOKEN } from '@tramvai/tokens-common';
7
7
  import { RENDER_SLOTS, EXTEND_RENDER } from '@tramvai/tokens-render';
8
8
  import { ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN, PAGE_SERVICE_TOKEN, ROUTER_TOKEN } from '@tramvai/tokens-router';
9
- import { resolveLazyComponent, useDi } from '@tramvai/react';
9
+ import { resolveLazyComponent, UniversalErrorBoundary, useDi } from '@tramvai/react';
10
10
  import flatten from '@tinkoff/utils/array/flatten';
11
11
  import noop from '@tinkoff/utils/function/noop';
12
12
  import { Subscription, ChildDispatcherContext, createEvent, createReducer } from '@tramvai/state';
13
13
  import { jsx } from 'react/jsx-runtime';
14
- import { createContext, memo, useContext, useMemo, useState, useEffect, createElement, Suspense } from 'react';
14
+ import { createContext, memo, Suspense, useContext, useMemo, useState, useEffect } from 'react';
15
15
  import applyOrReturn from '@tinkoff/utils/function/applyOrReturn';
16
16
  import { loadModule } from '@tinkoff/module-loader-client';
17
+ import { useUrl } from '@tramvai/module-router';
17
18
 
18
19
  const getChildProviders$2 = (appDi) => {
19
20
  const context = appDi.get(CONTEXT_TOKEN);
20
21
  return [
21
- {
22
+ provide({
22
23
  provide: commandLineListTokens.customerStart,
23
24
  multi: true,
24
- useFactory: ({ subscriptions, }) => {
25
+ useFactory: ({ subscriptions }) => {
25
26
  return async function resolveRootStateForChild() {
26
27
  if (!subscriptions) {
27
28
  return;
@@ -40,7 +41,7 @@ const getChildProviders$2 = (appDi) => {
40
41
  deps: {
41
42
  subscriptions: { token: CHILD_APP_INTERNAL_ROOT_STATE_SUBSCRIPTION_TOKEN, optional: true },
42
43
  },
43
- },
44
+ }),
44
45
  provide({
45
46
  provide: commandLineListTokens.clear,
46
47
  multi: true,
@@ -154,7 +155,6 @@ class SingletonDiManager {
154
155
  error,
155
156
  config,
156
157
  });
157
- return null;
158
158
  }
159
159
  }
160
160
  forEachChildDi(cb) {
@@ -223,7 +223,7 @@ const getChildProviders = (appDi) => {
223
223
  dispatcher,
224
224
  // context will be set later by the CONTEXT_TOKEN
225
225
  context: {},
226
- initialState,
226
+ initialState: initialState !== null && initialState !== void 0 ? initialState : { stores: [] },
227
227
  middlewares: flatten(middlewares || []),
228
228
  parentDispatcherContext,
229
229
  parentAllowedStores: flatten(parentAllowedStores || []),
@@ -284,7 +284,7 @@ class DiManager {
284
284
 
285
285
  class CommandLineRunner {
286
286
  constructor({ logger, rootCommandLineRunner, diManager, }) {
287
- this.log = logger('child-app:commandlinerunner');
287
+ this.log = logger('child-app:command-line-runner');
288
288
  this.rootCommandLineRunner = rootCommandLineRunner;
289
289
  this.diManager = diManager;
290
290
  }
@@ -384,10 +384,11 @@ const getModuleFederation = async (container, name = 'entry') => {
384
384
  };
385
385
 
386
386
  class ChildAppResolutionConfigManager {
387
- constructor({ configs, }) {
387
+ constructor({ configs, logger, }) {
388
388
  this.hasInitialized = false;
389
389
  this.rawConfigs = configs !== null && configs !== void 0 ? configs : [];
390
390
  this.mapping = new Map();
391
+ this.log = logger('child-app:resolution-config');
391
392
  }
392
393
  async init() {
393
394
  if (this.hasInitialized) {
@@ -398,10 +399,18 @@ class ChildAppResolutionConfigManager {
398
399
  }
399
400
  this.initPromise = (async () => {
400
401
  const configs = await Promise.all(this.rawConfigs.map((rawConfig) => {
401
- return applyOrReturn([], rawConfig);
402
+ return Promise.resolve()
403
+ .then(() => {
404
+ return applyOrReturn([], rawConfig);
405
+ })
406
+ .catch((error) => {
407
+ this.log.error(error, 'Failed while resolving resolution config');
408
+ });
402
409
  }));
403
410
  flatten(configs).forEach((config) => {
404
- this.mapping.set(config.name, config);
411
+ if (config) {
412
+ this.mapping.set(config.name, config);
413
+ }
405
414
  });
406
415
  this.hasInitialized = true;
407
416
  })();
@@ -411,7 +420,7 @@ class ChildAppResolutionConfigManager {
411
420
  var _a;
412
421
  const fromMapping = this.mapping.get(name);
413
422
  if (!fromMapping) {
414
- return null;
423
+ return;
415
424
  }
416
425
  const cfg = fromMapping.byTag[tag];
417
426
  if (process.env.NODE_ENV === 'development' && tag === 'debug' && !cfg) {
@@ -444,6 +453,7 @@ const sharedProviders = [
444
453
  useClass: ChildAppResolutionConfigManager,
445
454
  deps: {
446
455
  configs: { token: CHILD_APP_RESOLUTION_CONFIGS_TOKEN, optional: true },
456
+ logger: LOGGER_TOKEN,
447
457
  },
448
458
  }),
449
459
  provide({
@@ -460,8 +470,9 @@ const sharedProviders = [
460
470
  }),
461
471
  provide({
462
472
  provide: CHILD_APP_RESOLVE_CONFIG_TOKEN,
463
- useFactory: ({ envManager, rootBaseUrl, resolutionConfigManager }) => {
473
+ useFactory: ({ envManager, logger, rootBaseUrl, resolutionConfigManager }) => {
464
474
  const rawEnv = envManager.get('CHILD_APP_DEBUG');
475
+ const log = logger('child-app:resolve-config');
465
476
  const debug = new Map();
466
477
  rawEnv === null || rawEnv === void 0 ? void 0 : rawEnv.split(';').reduce((acc, entry) => {
467
478
  const [name, url] = entry.split('=');
@@ -473,7 +484,8 @@ const sharedProviders = [
473
484
  const req = { name, tag, version: request.version };
474
485
  const config = resolutionConfigManager.resolve(req);
475
486
  if (!config) {
476
- throw new Error(`Child-app "${name}" with tag "${tag}" has not found`);
487
+ log.error(`Child-app "${name}" with tag "${tag}" has not found`);
488
+ return;
477
489
  }
478
490
  const { version, baseUrl: configBaseUrl, client, server, css, withoutCss } = config;
479
491
  const baseUrl = (_b = (_a = debug.get(name)) !== null && _a !== void 0 ? _a : configBaseUrl) !== null && _b !== void 0 ? _b : rootBaseUrl;
@@ -505,6 +517,7 @@ const sharedProviders = [
505
517
  },
506
518
  deps: {
507
519
  envManager: ENV_MANAGER_TOKEN,
520
+ logger: LOGGER_TOKEN,
508
521
  rootBaseUrl: CHILD_APP_RESOLVE_BASE_URL_TOKEN,
509
522
  resolutionConfigManager: CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN,
510
523
  },
@@ -711,6 +724,7 @@ class PreloadManager {
711
724
  this.currentlyPreloaded = new Map();
712
725
  this.hasPreloadBefore = new Set();
713
726
  this.hasInitialized = false;
727
+ this.map = new Map();
714
728
  this.loader = loader;
715
729
  this.runner = runner;
716
730
  this.store = store;
@@ -720,29 +734,39 @@ class PreloadManager {
720
734
  async preload(request) {
721
735
  await this.init();
722
736
  const config = this.resolveExternalConfig(request);
737
+ if (!config) {
738
+ return;
739
+ }
723
740
  const { key } = config;
741
+ if (this.pageHasRendered) {
742
+ this.currentlyPreloaded.set(key, config);
743
+ }
724
744
  if (!this.isPreloaded(config)) {
745
+ if (this.map.has(key)) {
746
+ return this.map.get(key);
747
+ }
748
+ // TODO: remove after dropping support for react@<18 as it can handle hydration errors with Suspense
725
749
  // in case React render yet has not been executed do not load any external child-app app as
726
750
  // as it will lead to markup mismatch on markup hydration
727
751
  if (this.pageHasRendered) {
728
752
  // but in case render has happened load child-app as soon as possible
729
- try {
730
- await this.loader.load(config);
731
- await this.run('customer', config);
732
- await this.run('clear', config);
753
+ const promise = (async () => {
754
+ try {
755
+ await this.loader.load(config);
756
+ await this.run('customer', config);
757
+ await this.run('clear', config);
758
+ }
759
+ catch (error) { }
733
760
  this.hasPreloadBefore.add(key);
734
- }
735
- catch (error) { }
761
+ })();
762
+ this.map.set(key, promise);
763
+ return promise;
736
764
  }
737
765
  }
738
- if (this.pageHasRendered) {
739
- this.currentlyPreloaded.set(key, config);
740
- }
741
766
  }
742
767
  isPreloaded(request) {
743
768
  const config = this.resolveExternalConfig(request);
744
- const { key } = config;
745
- return this.hasPreloadBefore.has(key);
769
+ return !!config && this.hasPreloadBefore.has(config.key);
746
770
  }
747
771
  async runPreloaded() {
748
772
  await this.init();
@@ -764,6 +788,7 @@ class PreloadManager {
764
788
  async clearPreloaded() {
765
789
  if (this.pageHasLoaded) {
766
790
  this.currentlyPreloaded.clear();
791
+ this.map.clear();
767
792
  return;
768
793
  }
769
794
  this.pageHasLoaded = true;
@@ -772,6 +797,7 @@ class PreloadManager {
772
797
  promises.push(this.run('clear', config));
773
798
  });
774
799
  this.currentlyPreloaded.clear();
800
+ this.map.clear();
775
801
  await Promise.all(promises);
776
802
  }
777
803
  getPreloadedList() {
@@ -782,8 +808,10 @@ class PreloadManager {
782
808
  const { preloaded } = this.store.getState(ChildAppStore);
783
809
  preloaded.forEach((request) => {
784
810
  const config = this.resolveExternalConfig(request);
785
- this.currentlyPreloaded.set(config.key, config);
786
- this.hasPreloadBefore.add(config.key);
811
+ if (config) {
812
+ this.currentlyPreloaded.set(config.key, config);
813
+ this.hasPreloadBefore.add(config.key);
814
+ }
787
815
  });
788
816
  this.hasInitialized = true;
789
817
  }
@@ -810,8 +838,11 @@ class RenderManager {
810
838
  }
811
839
  getChildDi(request) {
812
840
  const config = this.resolveExternalConfig(request);
841
+ if (!config) {
842
+ throw new Error(`Child app "${request.name}" not found`);
843
+ }
813
844
  if (this.preloadManager.isPreloaded(request)) {
814
- return [this.diManager.getChildDi(config), null];
845
+ return [this.diManager.getChildDi(config), undefined];
815
846
  }
816
847
  this.log.warn({
817
848
  message: 'Child-app has been used but not preloaded before React render',
@@ -820,10 +851,7 @@ class RenderManager {
820
851
  const promiseDi = this.preloadManager.preload(request).then(() => {
821
852
  return this.diManager.getChildDi(config);
822
853
  });
823
- return [null, promiseDi];
824
- }
825
- async flush() {
826
- return false;
854
+ return [undefined, promiseDi];
827
855
  }
828
856
  clear() { }
829
857
  }
@@ -938,7 +966,9 @@ const browserProviders = [
938
966
  }),
939
967
  ];
940
968
 
941
- const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback, }) => {
969
+ const FailedChildAppFallback = ({ name, version, tag, fallback: Fallback, }) => {
970
+ const logger = useDi(LOGGER_TOKEN);
971
+ const log = logger('child-app:render');
942
972
  // On client-side hydration errors will be handled in `hydrateRoot` `onRecoverableError` property,
943
973
  // and update errors will be handled in Error Boundaries.
944
974
  //
@@ -948,7 +978,7 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
948
978
  // On server-side, we still use `renderToString`,
949
979
  // and need to manually log render errors for components, wrapped in Suspense Boundaries.
950
980
  if (typeof window === 'undefined') {
951
- logger.error({
981
+ log.error({
952
982
  event: 'failed-render',
953
983
  message: 'child-app failed to render, will try to recover during hydration',
954
984
  name,
@@ -958,21 +988,29 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
958
988
  }
959
989
  return Fallback ? jsx(Fallback, {}) : null;
960
990
  };
961
- const ChildApp = memo(({ name, version, tag, props, fallback }) => {
991
+ const ChildAppWrapper = ({ name, version, tag, props, fallback: Fallback, }) => {
962
992
  const renderManager = useContext(RenderContext);
963
- const resolveExternalConfig = useDi(CHILD_APP_RESOLVE_CONFIG_TOKEN);
964
993
  const logger = useDi(LOGGER_TOKEN);
965
994
  const log = logger('child-app:render');
966
- const [maybeDi, promiseDi] = useMemo(() => {
967
- return renderManager.getChildDi(resolveExternalConfig({ name, version, tag }));
968
- }, [name, version, tag, renderManager, resolveExternalConfig]);
995
+ const [maybeDi, maybePromiseDi] = useMemo(() => {
996
+ return renderManager.getChildDi({ name, version, tag });
997
+ }, [name, version, tag, renderManager]);
969
998
  const [di, setDi] = useState(maybeDi);
999
+ const [promiseDi, setPromiseDi] = useState(maybePromiseDi);
970
1000
  useEffect(() => {
971
1001
  if (!di && promiseDi) {
972
1002
  // any errors with loading child-app should be handled in some other place
973
- promiseDi.then(setDi).catch(noop);
1003
+ promiseDi
1004
+ .then(setDi)
1005
+ .finally(() => setPromiseDi(undefined))
1006
+ .catch(noop);
974
1007
  }
975
1008
  }, [di, promiseDi]);
1009
+ if (!di && promiseDi) {
1010
+ // in case child-app was not rendered on ssr
1011
+ // and we have to wait before it's loading
1012
+ return Fallback ? jsx(Fallback, {}) : null;
1013
+ }
976
1014
  if (!di) {
977
1015
  log.error({
978
1016
  event: 'not-found',
@@ -981,7 +1019,10 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
981
1019
  tag,
982
1020
  message: 'child-app was not initialized',
983
1021
  });
984
- return null;
1022
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES || typeof window !== 'undefined') {
1023
+ throw new Error(`Child-app was not initialized, check the loading error for child-app "${name}"`);
1024
+ }
1025
+ return Fallback ? jsx(Fallback, {}) : null;
985
1026
  }
986
1027
  try {
987
1028
  const Cmp = di.get({ token: CHILD_APP_INTERNAL_RENDER_TOKEN, optional: true });
@@ -995,14 +1036,7 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
995
1036
  });
996
1037
  return null;
997
1038
  }
998
- const result = createElement(Cmp, {
999
- di,
1000
- props,
1001
- });
1002
- if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1003
- return (jsx(Suspense, { fallback: jsx(FailedChildAppFallback, { name: name, version: version, tag: tag, logger: log, fallback: fallback }), children: result }));
1004
- }
1005
- return result;
1039
+ return jsx(Cmp, { di: di, props: props });
1006
1040
  }
1007
1041
  catch (error) {
1008
1042
  log.error({
@@ -1015,6 +1049,16 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
1015
1049
  });
1016
1050
  return null;
1017
1051
  }
1052
+ };
1053
+ const ChildApp = memo((config) => {
1054
+ const { fallback } = config;
1055
+ const url = useUrl();
1056
+ const result = (jsx(UniversalErrorBoundary, { url: url, fallback: fallback, children: jsx(ChildAppWrapper, { ...config }) }));
1057
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1058
+ const fallbackRender = FailedChildAppFallback(config);
1059
+ return jsx(Suspense, { fallback: fallbackRender, children: result });
1060
+ }
1061
+ return result;
1018
1062
  });
1019
1063
 
1020
1064
  let ChildAppModule = class ChildAppModule {
package/lib/server.es.js CHANGED
@@ -6,16 +6,17 @@ export * from '@tramvai/tokens-child-app';
6
6
  import { ACTION_PAGE_RUNNER_TOKEN, LOGGER_TOKEN, DISPATCHER_TOKEN, STORE_TOKEN, CONTEXT_TOKEN, DISPATCHER_CONTEXT_TOKEN, STORE_MIDDLEWARE, INITIAL_APP_STATE_TOKEN, COMBINE_REDUCERS, ENV_MANAGER_TOKEN, REGISTER_CLEAR_CACHE_TOKEN, CLEAR_CACHE_TOKEN, ENV_USED_TOKEN, CREATE_CACHE_TOKEN } from '@tramvai/tokens-common';
7
7
  import { RENDER_SLOTS, EXTEND_RENDER, ResourceType, ResourceSlot, RESOURCES_REGISTRY } from '@tramvai/tokens-render';
8
8
  import { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
9
- import { resolveLazyComponent, useDi } from '@tramvai/react';
9
+ import { resolveLazyComponent, UniversalErrorBoundary, useDi } from '@tramvai/react';
10
10
  import flatten from '@tinkoff/utils/array/flatten';
11
11
  import { ChildDispatcherContext, createEvent, createReducer } from '@tramvai/state';
12
12
  import { jsx } from 'react/jsx-runtime';
13
- import { createContext, memo, useContext, useMemo, useState, useEffect, createElement, Suspense } from 'react';
13
+ import { createContext, memo, Suspense, useContext, useMemo, useState, useEffect } from 'react';
14
14
  import applyOrReturn from '@tinkoff/utils/function/applyOrReturn';
15
15
  import { combineValidators, isUrl, endsWith } from '@tinkoff/env-validators';
16
16
  import { safeStringify } from '@tramvai/safe-strings';
17
17
  import { ServerLoader as ServerLoader$1 } from '@tinkoff/module-loader-server';
18
18
  import noop from '@tinkoff/utils/function/noop';
19
+ import { useUrl } from '@tramvai/module-router';
19
20
 
20
21
  const getChildProviders$2 = (appDi) => {
21
22
  return [
@@ -92,7 +93,6 @@ class SingletonDiManager {
92
93
  error,
93
94
  config,
94
95
  });
95
- return null;
96
96
  }
97
97
  }
98
98
  forEachChildDi(cb) {
@@ -161,7 +161,7 @@ const getChildProviders = (appDi) => {
161
161
  dispatcher,
162
162
  // context will be set later by the CONTEXT_TOKEN
163
163
  context: {},
164
- initialState,
164
+ initialState: initialState !== null && initialState !== void 0 ? initialState : { stores: [] },
165
165
  middlewares: flatten(middlewares || []),
166
166
  parentDispatcherContext,
167
167
  parentAllowedStores: flatten(parentAllowedStores || []),
@@ -222,7 +222,7 @@ class DiManager {
222
222
 
223
223
  class CommandLineRunner {
224
224
  constructor({ logger, rootCommandLineRunner, diManager, }) {
225
- this.log = logger('child-app:commandlinerunner');
225
+ this.log = logger('child-app:command-line-runner');
226
226
  this.rootCommandLineRunner = rootCommandLineRunner;
227
227
  this.diManager = diManager;
228
228
  }
@@ -322,10 +322,11 @@ const getModuleFederation = async (container, name = 'entry') => {
322
322
  };
323
323
 
324
324
  class ChildAppResolutionConfigManager {
325
- constructor({ configs, }) {
325
+ constructor({ configs, logger, }) {
326
326
  this.hasInitialized = false;
327
327
  this.rawConfigs = configs !== null && configs !== void 0 ? configs : [];
328
328
  this.mapping = new Map();
329
+ this.log = logger('child-app:resolution-config');
329
330
  }
330
331
  async init() {
331
332
  if (this.hasInitialized) {
@@ -336,10 +337,18 @@ class ChildAppResolutionConfigManager {
336
337
  }
337
338
  this.initPromise = (async () => {
338
339
  const configs = await Promise.all(this.rawConfigs.map((rawConfig) => {
339
- return applyOrReturn([], rawConfig);
340
+ return Promise.resolve()
341
+ .then(() => {
342
+ return applyOrReturn([], rawConfig);
343
+ })
344
+ .catch((error) => {
345
+ this.log.error(error, 'Failed while resolving resolution config');
346
+ });
340
347
  }));
341
348
  flatten(configs).forEach((config) => {
342
- this.mapping.set(config.name, config);
349
+ if (config) {
350
+ this.mapping.set(config.name, config);
351
+ }
343
352
  });
344
353
  this.hasInitialized = true;
345
354
  })();
@@ -349,7 +358,7 @@ class ChildAppResolutionConfigManager {
349
358
  var _a;
350
359
  const fromMapping = this.mapping.get(name);
351
360
  if (!fromMapping) {
352
- return null;
361
+ return;
353
362
  }
354
363
  const cfg = fromMapping.byTag[tag];
355
364
  if (process.env.NODE_ENV === 'development' && tag === 'debug' && !cfg) {
@@ -382,6 +391,7 @@ const sharedProviders = [
382
391
  useClass: ChildAppResolutionConfigManager,
383
392
  deps: {
384
393
  configs: { token: CHILD_APP_RESOLUTION_CONFIGS_TOKEN, optional: true },
394
+ logger: LOGGER_TOKEN,
385
395
  },
386
396
  }),
387
397
  provide({
@@ -398,8 +408,9 @@ const sharedProviders = [
398
408
  }),
399
409
  provide({
400
410
  provide: CHILD_APP_RESOLVE_CONFIG_TOKEN,
401
- useFactory: ({ envManager, rootBaseUrl, resolutionConfigManager }) => {
411
+ useFactory: ({ envManager, logger, rootBaseUrl, resolutionConfigManager }) => {
402
412
  const rawEnv = envManager.get('CHILD_APP_DEBUG');
413
+ const log = logger('child-app:resolve-config');
403
414
  const debug = new Map();
404
415
  rawEnv === null || rawEnv === void 0 ? void 0 : rawEnv.split(';').reduce((acc, entry) => {
405
416
  const [name, url] = entry.split('=');
@@ -411,7 +422,8 @@ const sharedProviders = [
411
422
  const req = { name, tag, version: request.version };
412
423
  const config = resolutionConfigManager.resolve(req);
413
424
  if (!config) {
414
- throw new Error(`Child-app "${name}" with tag "${tag}" has not found`);
425
+ log.error(`Child-app "${name}" with tag "${tag}" has not found`);
426
+ return;
415
427
  }
416
428
  const { version, baseUrl: configBaseUrl, client, server, css, withoutCss } = config;
417
429
  const baseUrl = (_b = (_a = debug.get(name)) !== null && _a !== void 0 ? _a : configBaseUrl) !== null && _b !== void 0 ? _b : rootBaseUrl;
@@ -443,6 +455,7 @@ const sharedProviders = [
443
455
  },
444
456
  deps: {
445
457
  envManager: ENV_MANAGER_TOKEN,
458
+ logger: LOGGER_TOKEN,
446
459
  rootBaseUrl: CHILD_APP_RESOLVE_BASE_URL_TOKEN,
447
460
  resolutionConfigManager: CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN,
448
461
  },
@@ -640,6 +653,9 @@ class PreloadManager {
640
653
  async preload(request) {
641
654
  await this.resolutionConfigManager.init();
642
655
  const config = this.resolveFullConfig(request);
656
+ if (!config) {
657
+ return;
658
+ }
643
659
  const { key } = config;
644
660
  if (this.map.has(key)) {
645
661
  await this.map.get(key);
@@ -664,8 +680,7 @@ class PreloadManager {
664
680
  }
665
681
  isPreloaded(request) {
666
682
  const config = this.resolveFullConfig(request);
667
- const { key } = config;
668
- return this.map.has(key);
683
+ return !!config && this.map.has(config.key);
669
684
  }
670
685
  async runPreloaded() {
671
686
  this.shouldRunImmediately = true;
@@ -748,7 +763,6 @@ class StateManager {
748
763
  class RenderManager {
749
764
  constructor({ logger, preloadManager, diManager, resolveFullConfig, }) {
750
765
  this.hasRenderedSet = new Set();
751
- this.loadingInProgress = new Map();
752
766
  this.log = logger('child-app:render');
753
767
  this.preloadManager = preloadManager;
754
768
  this.diManager = diManager;
@@ -756,37 +770,24 @@ class RenderManager {
756
770
  }
757
771
  getChildDi(request) {
758
772
  const config = this.resolveFullConfig(request);
773
+ if (!config) {
774
+ throw new Error(`Child app "${request.name}" not found`);
775
+ }
759
776
  this.hasRenderedSet.add(config.key);
760
777
  if (this.preloadManager.isPreloaded(request)) {
761
- return [this.diManager.getChildDi(config), null];
778
+ return [this.diManager.getChildDi(config), undefined];
762
779
  }
763
780
  this.log.warn({
764
781
  message: 'Child-app has been used but not preloaded before React render',
765
782
  request,
766
783
  });
767
- this.loadingInProgress.set(config.key, config);
768
- const promiseDi = this.preloadManager.preload(request).then(() => {
769
- return this.diManager.getChildDi(config);
770
- });
771
- return [null, promiseDi];
772
- }
773
- async flush() {
774
- const promises = [];
775
- for (const [_, request] of this.loadingInProgress.entries()) {
776
- promises.push(this.preloadManager.preload(request));
777
- }
778
- this.loadingInProgress.clear();
779
- if (promises.length) {
780
- await Promise.all(promises);
781
- return true;
782
- }
783
- return false;
784
+ return [undefined, undefined];
784
785
  }
785
786
  clear() {
786
787
  const preloadedList = this.preloadManager.getPreloadedList();
787
788
  for (const request of preloadedList) {
788
789
  const config = this.resolveFullConfig(request);
789
- if (!this.hasRenderedSet.has(config.key)) {
790
+ if (!config || !this.hasRenderedSet.has(config.key)) {
790
791
  this.log.warn({
791
792
  message: 'Child-app has been preloaded but not used in React render',
792
793
  request,
@@ -801,7 +802,11 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
801
802
  const log = logger('child-app:render:slots');
802
803
  const result = [];
803
804
  preloadManager.getPreloadedList().forEach((requestConfig) => {
805
+ var _a;
804
806
  const config = resolveFullConfig(requestConfig);
807
+ if (!config) {
808
+ return;
809
+ }
805
810
  const di = diManager.getChildDi(config);
806
811
  result.push({
807
812
  type: ResourceType.script,
@@ -815,7 +820,7 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
815
820
  result.push({
816
821
  type: ResourceType.style,
817
822
  slot: ResourceSlot.HEAD_CORE_STYLES,
818
- payload: config.css.entry,
823
+ payload: (_a = config.css.entry) !== null && _a !== void 0 ? _a : null,
819
824
  attrs: {
820
825
  'data-critical': 'true',
821
826
  },
@@ -957,7 +962,9 @@ const serverProviders = [
957
962
  }),
958
963
  ];
959
964
 
960
- const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback, }) => {
965
+ const FailedChildAppFallback = ({ name, version, tag, fallback: Fallback, }) => {
966
+ const logger = useDi(LOGGER_TOKEN);
967
+ const log = logger('child-app:render');
961
968
  // On client-side hydration errors will be handled in `hydrateRoot` `onRecoverableError` property,
962
969
  // and update errors will be handled in Error Boundaries.
963
970
  //
@@ -967,7 +974,7 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
967
974
  // On server-side, we still use `renderToString`,
968
975
  // and need to manually log render errors for components, wrapped in Suspense Boundaries.
969
976
  if (typeof window === 'undefined') {
970
- logger.error({
977
+ log.error({
971
978
  event: 'failed-render',
972
979
  message: 'child-app failed to render, will try to recover during hydration',
973
980
  name,
@@ -977,21 +984,29 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
977
984
  }
978
985
  return Fallback ? jsx(Fallback, {}) : null;
979
986
  };
980
- const ChildApp = memo(({ name, version, tag, props, fallback }) => {
987
+ const ChildAppWrapper = ({ name, version, tag, props, fallback: Fallback, }) => {
981
988
  const renderManager = useContext(RenderContext);
982
- const resolveExternalConfig = useDi(CHILD_APP_RESOLVE_CONFIG_TOKEN);
983
989
  const logger = useDi(LOGGER_TOKEN);
984
990
  const log = logger('child-app:render');
985
- const [maybeDi, promiseDi] = useMemo(() => {
986
- return renderManager.getChildDi(resolveExternalConfig({ name, version, tag }));
987
- }, [name, version, tag, renderManager, resolveExternalConfig]);
991
+ const [maybeDi, maybePromiseDi] = useMemo(() => {
992
+ return renderManager.getChildDi({ name, version, tag });
993
+ }, [name, version, tag, renderManager]);
988
994
  const [di, setDi] = useState(maybeDi);
995
+ const [promiseDi, setPromiseDi] = useState(maybePromiseDi);
989
996
  useEffect(() => {
990
997
  if (!di && promiseDi) {
991
998
  // any errors with loading child-app should be handled in some other place
992
- promiseDi.then(setDi).catch(noop);
999
+ promiseDi
1000
+ .then(setDi)
1001
+ .finally(() => setPromiseDi(undefined))
1002
+ .catch(noop);
993
1003
  }
994
1004
  }, [di, promiseDi]);
1005
+ if (!di && promiseDi) {
1006
+ // in case child-app was not rendered on ssr
1007
+ // and we have to wait before it's loading
1008
+ return Fallback ? jsx(Fallback, {}) : null;
1009
+ }
995
1010
  if (!di) {
996
1011
  log.error({
997
1012
  event: 'not-found',
@@ -1000,7 +1015,10 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
1000
1015
  tag,
1001
1016
  message: 'child-app was not initialized',
1002
1017
  });
1003
- return null;
1018
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES || typeof window !== 'undefined') {
1019
+ throw new Error(`Child-app was not initialized, check the loading error for child-app "${name}"`);
1020
+ }
1021
+ return Fallback ? jsx(Fallback, {}) : null;
1004
1022
  }
1005
1023
  try {
1006
1024
  const Cmp = di.get({ token: CHILD_APP_INTERNAL_RENDER_TOKEN, optional: true });
@@ -1014,14 +1032,7 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
1014
1032
  });
1015
1033
  return null;
1016
1034
  }
1017
- const result = createElement(Cmp, {
1018
- di,
1019
- props,
1020
- });
1021
- if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1022
- return (jsx(Suspense, { fallback: jsx(FailedChildAppFallback, { name: name, version: version, tag: tag, logger: log, fallback: fallback }), children: result }));
1023
- }
1024
- return result;
1035
+ return jsx(Cmp, { di: di, props: props });
1025
1036
  }
1026
1037
  catch (error) {
1027
1038
  log.error({
@@ -1034,6 +1045,16 @@ const ChildApp = memo(({ name, version, tag, props, fallback }) => {
1034
1045
  });
1035
1046
  return null;
1036
1047
  }
1048
+ };
1049
+ const ChildApp = memo((config) => {
1050
+ const { fallback } = config;
1051
+ const url = useUrl();
1052
+ const result = (jsx(UniversalErrorBoundary, { url: url, fallback: fallback, children: jsx(ChildAppWrapper, { ...config }) }));
1053
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1054
+ const fallbackRender = FailedChildAppFallback(config);
1055
+ return jsx(Suspense, { fallback: fallbackRender, children: result });
1056
+ }
1057
+ return result;
1037
1058
  });
1038
1059
 
1039
1060
  let ChildAppModule = class ChildAppModule {
package/lib/server.js CHANGED
@@ -19,6 +19,7 @@ var envValidators = require('@tinkoff/env-validators');
19
19
  var safeStrings = require('@tramvai/safe-strings');
20
20
  var moduleLoaderServer = require('@tinkoff/module-loader-server');
21
21
  var noop = require('@tinkoff/utils/function/noop');
22
+ var moduleRouter = require('@tramvai/module-router');
22
23
 
23
24
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
24
25
 
@@ -101,7 +102,6 @@ class SingletonDiManager {
101
102
  error,
102
103
  config,
103
104
  });
104
- return null;
105
105
  }
106
106
  }
107
107
  forEachChildDi(cb) {
@@ -170,7 +170,7 @@ const getChildProviders = (appDi) => {
170
170
  dispatcher,
171
171
  // context will be set later by the CONTEXT_TOKEN
172
172
  context: {},
173
- initialState,
173
+ initialState: initialState !== null && initialState !== void 0 ? initialState : { stores: [] },
174
174
  middlewares: flatten__default["default"](middlewares || []),
175
175
  parentDispatcherContext,
176
176
  parentAllowedStores: flatten__default["default"](parentAllowedStores || []),
@@ -231,7 +231,7 @@ class DiManager {
231
231
 
232
232
  class CommandLineRunner {
233
233
  constructor({ logger, rootCommandLineRunner, diManager, }) {
234
- this.log = logger('child-app:commandlinerunner');
234
+ this.log = logger('child-app:command-line-runner');
235
235
  this.rootCommandLineRunner = rootCommandLineRunner;
236
236
  this.diManager = diManager;
237
237
  }
@@ -331,10 +331,11 @@ const getModuleFederation = async (container, name = 'entry') => {
331
331
  };
332
332
 
333
333
  class ChildAppResolutionConfigManager {
334
- constructor({ configs, }) {
334
+ constructor({ configs, logger, }) {
335
335
  this.hasInitialized = false;
336
336
  this.rawConfigs = configs !== null && configs !== void 0 ? configs : [];
337
337
  this.mapping = new Map();
338
+ this.log = logger('child-app:resolution-config');
338
339
  }
339
340
  async init() {
340
341
  if (this.hasInitialized) {
@@ -345,10 +346,18 @@ class ChildAppResolutionConfigManager {
345
346
  }
346
347
  this.initPromise = (async () => {
347
348
  const configs = await Promise.all(this.rawConfigs.map((rawConfig) => {
348
- return applyOrReturn__default["default"]([], rawConfig);
349
+ return Promise.resolve()
350
+ .then(() => {
351
+ return applyOrReturn__default["default"]([], rawConfig);
352
+ })
353
+ .catch((error) => {
354
+ this.log.error(error, 'Failed while resolving resolution config');
355
+ });
349
356
  }));
350
357
  flatten__default["default"](configs).forEach((config) => {
351
- this.mapping.set(config.name, config);
358
+ if (config) {
359
+ this.mapping.set(config.name, config);
360
+ }
352
361
  });
353
362
  this.hasInitialized = true;
354
363
  })();
@@ -358,7 +367,7 @@ class ChildAppResolutionConfigManager {
358
367
  var _a;
359
368
  const fromMapping = this.mapping.get(name);
360
369
  if (!fromMapping) {
361
- return null;
370
+ return;
362
371
  }
363
372
  const cfg = fromMapping.byTag[tag];
364
373
  if (process.env.NODE_ENV === 'development' && tag === 'debug' && !cfg) {
@@ -391,6 +400,7 @@ const sharedProviders = [
391
400
  useClass: ChildAppResolutionConfigManager,
392
401
  deps: {
393
402
  configs: { token: tokensChildApp.CHILD_APP_RESOLUTION_CONFIGS_TOKEN, optional: true },
403
+ logger: tokensCommon.LOGGER_TOKEN,
394
404
  },
395
405
  }),
396
406
  core.provide({
@@ -407,8 +417,9 @@ const sharedProviders = [
407
417
  }),
408
418
  core.provide({
409
419
  provide: tokensChildApp.CHILD_APP_RESOLVE_CONFIG_TOKEN,
410
- useFactory: ({ envManager, rootBaseUrl, resolutionConfigManager }) => {
420
+ useFactory: ({ envManager, logger, rootBaseUrl, resolutionConfigManager }) => {
411
421
  const rawEnv = envManager.get('CHILD_APP_DEBUG');
422
+ const log = logger('child-app:resolve-config');
412
423
  const debug = new Map();
413
424
  rawEnv === null || rawEnv === void 0 ? void 0 : rawEnv.split(';').reduce((acc, entry) => {
414
425
  const [name, url] = entry.split('=');
@@ -420,7 +431,8 @@ const sharedProviders = [
420
431
  const req = { name, tag, version: request.version };
421
432
  const config = resolutionConfigManager.resolve(req);
422
433
  if (!config) {
423
- throw new Error(`Child-app "${name}" with tag "${tag}" has not found`);
434
+ log.error(`Child-app "${name}" with tag "${tag}" has not found`);
435
+ return;
424
436
  }
425
437
  const { version, baseUrl: configBaseUrl, client, server, css, withoutCss } = config;
426
438
  const baseUrl = (_b = (_a = debug.get(name)) !== null && _a !== void 0 ? _a : configBaseUrl) !== null && _b !== void 0 ? _b : rootBaseUrl;
@@ -452,6 +464,7 @@ const sharedProviders = [
452
464
  },
453
465
  deps: {
454
466
  envManager: tokensCommon.ENV_MANAGER_TOKEN,
467
+ logger: tokensCommon.LOGGER_TOKEN,
455
468
  rootBaseUrl: tokensChildApp.CHILD_APP_RESOLVE_BASE_URL_TOKEN,
456
469
  resolutionConfigManager: tokensChildApp.CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN,
457
470
  },
@@ -649,6 +662,9 @@ class PreloadManager {
649
662
  async preload(request) {
650
663
  await this.resolutionConfigManager.init();
651
664
  const config = this.resolveFullConfig(request);
665
+ if (!config) {
666
+ return;
667
+ }
652
668
  const { key } = config;
653
669
  if (this.map.has(key)) {
654
670
  await this.map.get(key);
@@ -673,8 +689,7 @@ class PreloadManager {
673
689
  }
674
690
  isPreloaded(request) {
675
691
  const config = this.resolveFullConfig(request);
676
- const { key } = config;
677
- return this.map.has(key);
692
+ return !!config && this.map.has(config.key);
678
693
  }
679
694
  async runPreloaded() {
680
695
  this.shouldRunImmediately = true;
@@ -757,7 +772,6 @@ class StateManager {
757
772
  class RenderManager {
758
773
  constructor({ logger, preloadManager, diManager, resolveFullConfig, }) {
759
774
  this.hasRenderedSet = new Set();
760
- this.loadingInProgress = new Map();
761
775
  this.log = logger('child-app:render');
762
776
  this.preloadManager = preloadManager;
763
777
  this.diManager = diManager;
@@ -765,37 +779,24 @@ class RenderManager {
765
779
  }
766
780
  getChildDi(request) {
767
781
  const config = this.resolveFullConfig(request);
782
+ if (!config) {
783
+ throw new Error(`Child app "${request.name}" not found`);
784
+ }
768
785
  this.hasRenderedSet.add(config.key);
769
786
  if (this.preloadManager.isPreloaded(request)) {
770
- return [this.diManager.getChildDi(config), null];
787
+ return [this.diManager.getChildDi(config), undefined];
771
788
  }
772
789
  this.log.warn({
773
790
  message: 'Child-app has been used but not preloaded before React render',
774
791
  request,
775
792
  });
776
- this.loadingInProgress.set(config.key, config);
777
- const promiseDi = this.preloadManager.preload(request).then(() => {
778
- return this.diManager.getChildDi(config);
779
- });
780
- return [null, promiseDi];
781
- }
782
- async flush() {
783
- const promises = [];
784
- for (const [_, request] of this.loadingInProgress.entries()) {
785
- promises.push(this.preloadManager.preload(request));
786
- }
787
- this.loadingInProgress.clear();
788
- if (promises.length) {
789
- await Promise.all(promises);
790
- return true;
791
- }
792
- return false;
793
+ return [undefined, undefined];
793
794
  }
794
795
  clear() {
795
796
  const preloadedList = this.preloadManager.getPreloadedList();
796
797
  for (const request of preloadedList) {
797
798
  const config = this.resolveFullConfig(request);
798
- if (!this.hasRenderedSet.has(config.key)) {
799
+ if (!config || !this.hasRenderedSet.has(config.key)) {
799
800
  this.log.warn({
800
801
  message: 'Child-app has been preloaded but not used in React render',
801
802
  request,
@@ -810,7 +811,11 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
810
811
  const log = logger('child-app:render:slots');
811
812
  const result = [];
812
813
  preloadManager.getPreloadedList().forEach((requestConfig) => {
814
+ var _a;
813
815
  const config = resolveFullConfig(requestConfig);
816
+ if (!config) {
817
+ return;
818
+ }
814
819
  const di = diManager.getChildDi(config);
815
820
  result.push({
816
821
  type: tokensRender.ResourceType.script,
@@ -824,7 +829,7 @@ const registerChildAppRenderSlots = ({ logger, diManager, resolveFullConfig, pre
824
829
  result.push({
825
830
  type: tokensRender.ResourceType.style,
826
831
  slot: tokensRender.ResourceSlot.HEAD_CORE_STYLES,
827
- payload: config.css.entry,
832
+ payload: (_a = config.css.entry) !== null && _a !== void 0 ? _a : null,
828
833
  attrs: {
829
834
  'data-critical': 'true',
830
835
  },
@@ -966,7 +971,9 @@ const serverProviders = [
966
971
  }),
967
972
  ];
968
973
 
969
- const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback, }) => {
974
+ const FailedChildAppFallback = ({ name, version, tag, fallback: Fallback, }) => {
975
+ const logger = react$1.useDi(tokensCommon.LOGGER_TOKEN);
976
+ const log = logger('child-app:render');
970
977
  // On client-side hydration errors will be handled in `hydrateRoot` `onRecoverableError` property,
971
978
  // and update errors will be handled in Error Boundaries.
972
979
  //
@@ -976,7 +983,7 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
976
983
  // On server-side, we still use `renderToString`,
977
984
  // and need to manually log render errors for components, wrapped in Suspense Boundaries.
978
985
  if (typeof window === 'undefined') {
979
- logger.error({
986
+ log.error({
980
987
  event: 'failed-render',
981
988
  message: 'child-app failed to render, will try to recover during hydration',
982
989
  name,
@@ -986,21 +993,29 @@ const FailedChildAppFallback = ({ name, version, tag, logger, fallback: Fallback
986
993
  }
987
994
  return Fallback ? jsxRuntime.jsx(Fallback, {}) : null;
988
995
  };
989
- const ChildApp = react.memo(({ name, version, tag, props, fallback }) => {
996
+ const ChildAppWrapper = ({ name, version, tag, props, fallback: Fallback, }) => {
990
997
  const renderManager = react.useContext(RenderContext);
991
- const resolveExternalConfig = react$1.useDi(tokensChildApp.CHILD_APP_RESOLVE_CONFIG_TOKEN);
992
998
  const logger = react$1.useDi(tokensCommon.LOGGER_TOKEN);
993
999
  const log = logger('child-app:render');
994
- const [maybeDi, promiseDi] = react.useMemo(() => {
995
- return renderManager.getChildDi(resolveExternalConfig({ name, version, tag }));
996
- }, [name, version, tag, renderManager, resolveExternalConfig]);
1000
+ const [maybeDi, maybePromiseDi] = react.useMemo(() => {
1001
+ return renderManager.getChildDi({ name, version, tag });
1002
+ }, [name, version, tag, renderManager]);
997
1003
  const [di, setDi] = react.useState(maybeDi);
1004
+ const [promiseDi, setPromiseDi] = react.useState(maybePromiseDi);
998
1005
  react.useEffect(() => {
999
1006
  if (!di && promiseDi) {
1000
1007
  // any errors with loading child-app should be handled in some other place
1001
- promiseDi.then(setDi).catch(noop__default["default"]);
1008
+ promiseDi
1009
+ .then(setDi)
1010
+ .finally(() => setPromiseDi(undefined))
1011
+ .catch(noop__default["default"]);
1002
1012
  }
1003
1013
  }, [di, promiseDi]);
1014
+ if (!di && promiseDi) {
1015
+ // in case child-app was not rendered on ssr
1016
+ // and we have to wait before it's loading
1017
+ return Fallback ? jsxRuntime.jsx(Fallback, {}) : null;
1018
+ }
1004
1019
  if (!di) {
1005
1020
  log.error({
1006
1021
  event: 'not-found',
@@ -1009,7 +1024,10 @@ const ChildApp = react.memo(({ name, version, tag, props, fallback }) => {
1009
1024
  tag,
1010
1025
  message: 'child-app was not initialized',
1011
1026
  });
1012
- return null;
1027
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES || typeof window !== 'undefined') {
1028
+ throw new Error(`Child-app was not initialized, check the loading error for child-app "${name}"`);
1029
+ }
1030
+ return Fallback ? jsxRuntime.jsx(Fallback, {}) : null;
1013
1031
  }
1014
1032
  try {
1015
1033
  const Cmp = di.get({ token: tokensChildApp.CHILD_APP_INTERNAL_RENDER_TOKEN, optional: true });
@@ -1023,14 +1041,7 @@ const ChildApp = react.memo(({ name, version, tag, props, fallback }) => {
1023
1041
  });
1024
1042
  return null;
1025
1043
  }
1026
- const result = react.createElement(Cmp, {
1027
- di,
1028
- props,
1029
- });
1030
- if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1031
- return (jsxRuntime.jsx(react.Suspense, { fallback: jsxRuntime.jsx(FailedChildAppFallback, { name: name, version: version, tag: tag, logger: log, fallback: fallback }), children: result }));
1032
- }
1033
- return result;
1044
+ return jsxRuntime.jsx(Cmp, { di: di, props: props });
1034
1045
  }
1035
1046
  catch (error) {
1036
1047
  log.error({
@@ -1043,6 +1054,16 @@ const ChildApp = react.memo(({ name, version, tag, props, fallback }) => {
1043
1054
  });
1044
1055
  return null;
1045
1056
  }
1057
+ };
1058
+ const ChildApp = react.memo((config) => {
1059
+ const { fallback } = config;
1060
+ const url = moduleRouter.useUrl();
1061
+ const result = (jsxRuntime.jsx(react$1.UniversalErrorBoundary, { url: url, fallback: fallback, children: jsxRuntime.jsx(ChildAppWrapper, { ...config }) }));
1062
+ if (process.env.__TRAMVAI_CONCURRENT_FEATURES) {
1063
+ const fallbackRender = FailedChildAppFallback(config);
1064
+ return jsxRuntime.jsx(react.Suspense, { fallback: fallbackRender, children: result });
1065
+ }
1066
+ return result;
1046
1067
  });
1047
1068
 
1048
1069
  exports.ChildAppModule = class ChildAppModule {
@@ -11,7 +11,7 @@ export declare class DiManager implements ChildAppDiManager {
11
11
  loader: ChildAppLoader;
12
12
  singletonDiManager: ChildAppDiManager;
13
13
  });
14
- getChildDi(config: ChildAppFinalConfig): Container | ChildContainer;
14
+ getChildDi(config: ChildAppFinalConfig): Container | ChildContainer | undefined;
15
15
  forEachChildDi(cb: (di: Container) => void): void;
16
16
  private resolveDi;
17
17
  }
@@ -1,2 +1,2 @@
1
1
  import type { ChildAppReactConfig } from '@tramvai/tokens-child-app';
2
- export declare const ChildApp: import("react").MemoExoticComponent<({ name, version, tag, props, fallback }: ChildAppReactConfig) => JSX.Element>;
2
+ export declare const ChildApp: import("react").MemoExoticComponent<(config: ChildAppReactConfig) => JSX.Element>;
@@ -1,2 +1,2 @@
1
1
  import type { ChildAppRenderManager } from '@tramvai/tokens-child-app';
2
- export declare const RenderContext: import("react").Context<ChildAppRenderManager>;
2
+ export declare const RenderContext: import("react").Context<ChildAppRenderManager | null>;
@@ -1,15 +1,18 @@
1
1
  import type { ChildAppRequestConfig, CHILD_APP_RESOLUTION_CONFIGS_TOKEN, CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN, ResolutionConfig } from '@tramvai/tokens-child-app';
2
2
  import type { ExtractDependencyType, ExtractTokenType } from '@tinkoff/dippy';
3
+ import type { LOGGER_TOKEN } from '@tramvai/tokens-common';
3
4
  type Interface = ExtractTokenType<typeof CHILD_APP_RESOLUTION_CONFIG_MANAGER_TOKEN>;
4
5
  export declare class ChildAppResolutionConfigManager implements Interface {
5
6
  private rawConfigs;
6
7
  private mapping;
8
+ private log;
7
9
  private hasInitialized;
8
- private initPromise;
9
- constructor({ configs, }: {
10
+ private initPromise?;
11
+ constructor({ configs, logger, }: {
10
12
  configs: ExtractDependencyType<typeof CHILD_APP_RESOLUTION_CONFIGS_TOKEN> | null;
13
+ logger: typeof LOGGER_TOKEN;
11
14
  });
12
15
  init(): Promise<void>;
13
- resolve({ name, version, tag }: ChildAppRequestConfig): ResolutionConfig;
16
+ resolve({ name, version, tag }: ChildAppRequestConfig): ResolutionConfig | undefined;
14
17
  }
15
18
  export {};
@@ -11,7 +11,7 @@ export declare class SingletonDiManager implements ChildAppDiManager {
11
11
  appDi: Container;
12
12
  loader: ChildAppLoader;
13
13
  });
14
- getChildDi(config: ChildAppFinalConfig): Container;
14
+ getChildDi(config: ChildAppFinalConfig): Container | undefined;
15
15
  forEachChildDi(cb: (di: Container) => void): void;
16
16
  private resolveDi;
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-child-app",
3
- "version": "2.61.1",
3
+ "version": "2.63.0",
4
4
  "description": "Module for child apps",
5
5
  "browser": {
6
6
  "./lib/server.js": "./lib/browser.js",
@@ -30,21 +30,22 @@
30
30
  "dependencies": {
31
31
  "@tinkoff/env-validators": "0.1.4",
32
32
  "@tinkoff/module-loader-client": "0.4.4",
33
- "@tinkoff/module-loader-server": "0.5.5",
34
- "@tramvai/child-app-core": "2.61.1",
33
+ "@tinkoff/module-loader-server": "0.5.6",
34
+ "@tramvai/child-app-core": "2.63.0",
35
+ "@tramvai/module-router": "2.63.0",
35
36
  "@tramvai/safe-strings": "0.5.6",
36
- "@tramvai/tokens-child-app": "2.61.1"
37
+ "@tramvai/tokens-child-app": "2.63.0"
37
38
  },
38
39
  "devDependencies": {},
39
40
  "peerDependencies": {
40
41
  "@tinkoff/dippy": "0.8.11",
41
42
  "@tinkoff/utils": "^2.1.2",
42
- "@tramvai/core": "2.61.1",
43
- "@tramvai/state": "2.61.1",
44
- "@tramvai/react": "2.61.1",
45
- "@tramvai/tokens-common": "2.61.1",
46
- "@tramvai/tokens-render": "2.61.1",
47
- "@tramvai/tokens-router": "2.61.1",
43
+ "@tramvai/core": "2.63.0",
44
+ "@tramvai/state": "2.63.0",
45
+ "@tramvai/react": "2.63.0",
46
+ "@tramvai/tokens-common": "2.63.0",
47
+ "@tramvai/tokens-render": "2.63.0",
48
+ "@tramvai/tokens-router": "2.63.0",
48
49
  "react": ">=16.14.0",
49
50
  "react-dom": ">=16.14.0",
50
51
  "object-assign": "^4.1.1",