@tramvai/module-router 2.40.0 → 2.44.2
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 +48 -1
- package/__migrations__/1-link-from-module.js +49 -0
- package/lib/components/link.d.ts +19 -0
- package/lib/hooks/usePageService.d.ts +3 -0
- package/lib/hooks/usePrefetch.browser.d.ts +5 -0
- package/lib/hooks/usePrefetch.d.ts +5 -0
- package/lib/index.browser.js +243 -10
- package/lib/index.d.ts +22 -1
- package/lib/index.es.js +95 -8
- package/lib/index.js +93 -7
- package/lib/modules/guards/common/loadBundle.d.ts +5 -2
- package/lib/modules/tokens/browser/index.d.ts +19 -0
- package/lib/modules/tokens/browser/prefetch.d.ts +19 -0
- package/lib/modules/tokens/server/prefetch.d.ts +3 -0
- package/lib/modules/tokens/server/routerOptions.d.ts +1 -1
- package/lib/modules/utils/forRoot.d.ts +1 -1
- package/lib/services/page.d.ts +3 -3
- package/lib/test/unit/guard.d.ts +1 -1
- package/lib/utils/requestIdleCallback.d.ts +2 -0
- package/package.json +20 -17
package/README.md
CHANGED
|
@@ -268,7 +268,7 @@ const action = declareAction({
|
|
|
268
268
|
|
|
269
269
|
### Working with navigation in React components
|
|
270
270
|
|
|
271
|
-
You can work with routing inside React components using hooks and components - `useNavigate
|
|
271
|
+
You can work with routing inside React components using hooks and components - `useNavigate` and `useRoute` from the [@tinkoff/router](references/libs/router.md#интеграция-с-react)
|
|
272
272
|
|
|
273
273
|
<p>
|
|
274
274
|
<details>
|
|
@@ -279,6 +279,53 @@ You can work with routing inside React components using hooks and components - `
|
|
|
279
279
|
</details>
|
|
280
280
|
</p>
|
|
281
281
|
|
|
282
|
+
#### Link
|
|
283
|
+
|
|
284
|
+
A wrapper for a react component that makes it clickable
|
|
285
|
+
|
|
286
|
+
> If the react component is passed to the Link as children, then this passed component will be rendered and the `href`, `onClick` props will be passed as props to that component and they should be used to make the navigation. Otherwise, the `<a>` tag will be rendered with children as a child.
|
|
287
|
+
> Your passed component need to be wrapped in the `forwardRef` for routes assets prefetching.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { Link } from '@tramvai/module-router';
|
|
291
|
+
import CustomLink from '@custom-scope/link';
|
|
292
|
+
|
|
293
|
+
export const Component = () => {
|
|
294
|
+
return (
|
|
295
|
+
<Link url="/test/">
|
|
296
|
+
<CustomLink />
|
|
297
|
+
</Link>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const WrapLink = () => {
|
|
302
|
+
return <Link url="/test/">Click me</Link>;
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
##### Page resources prefetch
|
|
307
|
+
|
|
308
|
+
`Link` component will try to prefetch resources for passed `url`, if this `url` is handled by the application router.
|
|
309
|
+
|
|
310
|
+
It will help to make subsequent page-loads faster because target page assets already be saved in browser cache.
|
|
311
|
+
|
|
312
|
+
How it works:
|
|
313
|
+
|
|
314
|
+
- Component determines when it is in the viewport (using `Intersection Observer`)
|
|
315
|
+
- waits until the browser is idle (using `requestIdleCallback`)
|
|
316
|
+
- checks if the user isn't on a slow connection (using `navigator.connection.effectiveType`) or has data-saver enabled (using `navigator.connection.saveData`)
|
|
317
|
+
- triggers page resources (js, css) prefetching
|
|
318
|
+
|
|
319
|
+
Main reference for this feature - [quicklink](https://github.com/GoogleChromeLabs/quicklink) library.
|
|
320
|
+
|
|
321
|
+
If you want to disable this behaviour, pass `prefetch={false}` property.
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
export const WrapLink = () => {
|
|
325
|
+
return <Link url="/test/" prefetch={false}>Click me</Link>;
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
282
329
|
### How to set static routes
|
|
283
330
|
|
|
284
331
|
[RouterModule](references/modules/router/base.md) allows you to add new routes when configuring your application. The second way is to pass static routes to DI via the `ROUTES_TOKEN` token.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tslib = require('tslib');
|
|
4
|
+
|
|
5
|
+
var _1LinkFromModule = (function (api) { return tslib.__awaiter(void 0, void 0, void 0, function () {
|
|
6
|
+
return tslib.__generator(this, function (_a) {
|
|
7
|
+
switch (_a.label) {
|
|
8
|
+
case 0: return [4 /*yield*/, api.transform(function (_a, _b, _c) {
|
|
9
|
+
var source = _a.source;
|
|
10
|
+
var j = _b.j;
|
|
11
|
+
var printOptions = _c.printOptions;
|
|
12
|
+
var parsed = j(source);
|
|
13
|
+
var routerLibImport = parsed.find(j.ImportDeclaration, {
|
|
14
|
+
source: { value: '@tinkoff/router' },
|
|
15
|
+
});
|
|
16
|
+
if (routerLibImport) {
|
|
17
|
+
var linkImportSpecifier_1;
|
|
18
|
+
routerLibImport.forEach(function (p) {
|
|
19
|
+
var _a;
|
|
20
|
+
(_a = p.node.specifiers) === null || _a === void 0 ? void 0 : _a.forEach(function (specifier) {
|
|
21
|
+
if ('imported' in specifier && specifier.imported.name === 'Link') {
|
|
22
|
+
linkImportSpecifier_1 = specifier;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
if (linkImportSpecifier_1) {
|
|
27
|
+
routerLibImport.forEach(function (p) {
|
|
28
|
+
var _a, _b;
|
|
29
|
+
// eslint-disable-next-line no-param-reassign
|
|
30
|
+
p.node.specifiers = (_a = p.node.specifiers) === null || _a === void 0 ? void 0 : _a.filter(function (specifier) {
|
|
31
|
+
return !('imported' in specifier && specifier.imported.name === 'Link');
|
|
32
|
+
});
|
|
33
|
+
if (((_b = p.node.specifiers) === null || _b === void 0 ? void 0 : _b.length) === 0) {
|
|
34
|
+
routerLibImport.remove();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
parsed.addImport(j.importDeclaration([linkImportSpecifier_1], j.stringLiteral('@tramvai/module-router')));
|
|
38
|
+
return parsed.toSource(printOptions);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})];
|
|
42
|
+
case 1:
|
|
43
|
+
_a.sent();
|
|
44
|
+
return [2 /*return*/];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}); });
|
|
48
|
+
|
|
49
|
+
module.exports = _1LinkFromModule;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
children?: any;
|
|
3
|
+
url: string;
|
|
4
|
+
query?: Record<string, string>;
|
|
5
|
+
replace?: boolean;
|
|
6
|
+
target?: string;
|
|
7
|
+
onClick?: Function;
|
|
8
|
+
navigateOptions?: Record<string, any>;
|
|
9
|
+
/**
|
|
10
|
+
* @description enable or disable target page resources prefetching
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
prefetch?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare function Link(props: Props): JSX.Element;
|
|
16
|
+
declare namespace Link {
|
|
17
|
+
var displayName: string;
|
|
18
|
+
}
|
|
19
|
+
export { Link };
|
package/lib/index.browser.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { provide, commandLineListTokens, Module, DI_TOKEN, COMMAND_LINE_RUNNER_TOKEN } from '@tramvai/core';
|
|
2
|
-
import { Provider, setLogger, NoSpaRouter, Router } from '@tinkoff/router';
|
|
3
|
-
export {
|
|
1
|
+
import { provide, commandLineListTokens, createToken as createToken$1, Module, DI_TOKEN, COMMAND_LINE_RUNNER_TOKEN } from '@tramvai/core';
|
|
2
|
+
import { Provider, setLogger, NoSpaRouter, Router, useNavigate, useRoute } from '@tinkoff/router';
|
|
3
|
+
export { Provider, useNavigate, useRoute, useRouter, useUrl } from '@tinkoff/router';
|
|
4
4
|
import noop from '@tinkoff/utils/function/noop';
|
|
5
|
-
import { LOGGER_TOKEN, BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_CONTEXT_TOKEN, CONTEXT_TOKEN, COMPONENT_REGISTRY_TOKEN, COMBINE_REDUCERS, STORE_TOKEN, ACTION_PAGE_RUNNER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
-
import { ROUTER_GUARD_TOKEN, ROUTE_TRANSFORM_TOKEN, ROUTER_TOKEN, ROUTES_TOKEN, ROUTE_RESOLVE_TOKEN, ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN,
|
|
5
|
+
import { LOGGER_TOKEN, BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_TOKEN, DISPATCHER_CONTEXT_TOKEN, CONTEXT_TOKEN, COMPONENT_REGISTRY_TOKEN, COMBINE_REDUCERS, STORE_TOKEN, ACTION_PAGE_RUNNER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
+
import { ROUTER_GUARD_TOKEN, PAGE_SERVICE_TOKEN, ROUTE_TRANSFORM_TOKEN, ROUTER_TOKEN, ROUTES_TOKEN, ROUTE_RESOLVE_TOKEN, ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN, LINK_PREFETCH_MANAGER_TOKEN } from '@tramvai/tokens-router';
|
|
7
7
|
export * from '@tramvai/tokens-router';
|
|
8
8
|
import { EXTEND_RENDER, TRAMVAI_RENDER_MODE } from '@tramvai/tokens-render';
|
|
9
9
|
import { createEvent, createReducer } from '@tramvai/state';
|
|
10
10
|
import flatten from '@tinkoff/utils/array/flatten';
|
|
11
11
|
import { createToken } from '@tinkoff/dippy';
|
|
12
|
+
import isArray from '@tinkoff/utils/is/array';
|
|
13
|
+
import { resolveLazyComponent, useDi } from '@tramvai/react';
|
|
12
14
|
import identity from '@tinkoff/utils/function/identity';
|
|
13
15
|
import compose from '@tinkoff/utils/function/compose';
|
|
14
16
|
import replace from '@tinkoff/utils/string/replace';
|
|
15
17
|
import { jsx } from 'react/jsx-runtime';
|
|
16
18
|
import { isFileSystemPageComponent, fileSystemPagesEnabled, getStaticFileSystemPages, fileSystemPageToRoute } from '@tramvai/experiments';
|
|
19
|
+
import { addQuery } from '@tinkoff/url';
|
|
17
20
|
import uniq from '@tinkoff/utils/array/uniq';
|
|
18
21
|
import { isRedirectFoundError, isNotFoundError } from '@tinkoff/errors';
|
|
19
22
|
import { __decorate } from 'tslib';
|
|
20
23
|
import { CHILD_APP_INTERNAL_ROOT_STATE_ALLOWED_STORE_TOKEN } from '@tramvai/tokens-child-app';
|
|
24
|
+
import { useEffect, useState, useCallback, isValidElement, cloneElement } from 'react';
|
|
21
25
|
|
|
22
26
|
const setCurrentNavigation = createEvent('SET_CURRENT_ROUTE');
|
|
23
27
|
const setUrlOnRehydrate = createEvent('SET_URL_ON_REHYDRATE');
|
|
@@ -64,7 +68,7 @@ const afterUpdateCurrentHooksToken = createToken('router afterUpdateCurrentHooks
|
|
|
64
68
|
const routeTransformToken = createToken('router finalRouteTransform');
|
|
65
69
|
createToken('router bundleInfoAdditional');
|
|
66
70
|
|
|
67
|
-
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcherContext, }) => {
|
|
71
|
+
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcher, dispatcherContext, pageService, }) => {
|
|
68
72
|
const log = logger('route:load-bundles');
|
|
69
73
|
return async ({ to }) => {
|
|
70
74
|
var _a;
|
|
@@ -72,7 +76,7 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
72
76
|
event: 'load-bundle',
|
|
73
77
|
route: to,
|
|
74
78
|
});
|
|
75
|
-
const { bundle, pageComponent } = to.config;
|
|
79
|
+
const { bundle, pageComponent, nestedLayoutComponent } = to.config;
|
|
76
80
|
if (!bundleManager.has(bundle, pageComponent)) {
|
|
77
81
|
log.info({
|
|
78
82
|
event: 'load-bundle-not-found',
|
|
@@ -87,12 +91,33 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
87
91
|
// несмотря на наличие бандла в разметке, все равно мы должны дождаться
|
|
88
92
|
// его загрузки перед рендером
|
|
89
93
|
try {
|
|
90
|
-
const
|
|
94
|
+
const promises = [];
|
|
95
|
+
// for file-system pages bundle is virtual, so it will be loaded in parallel with nested layout
|
|
96
|
+
// for classic bundles, page component will be loaded after bundle loading
|
|
97
|
+
promises.push(bundleManager.get(bundle, pageComponent));
|
|
98
|
+
if (nestedLayoutComponent) {
|
|
99
|
+
promises.push(resolveLazyComponent(pageService.resolveComponentFromConfig('nestedLayout')));
|
|
100
|
+
}
|
|
101
|
+
const [resolvedBundle, resolvedLayout] = await Promise.all(promises);
|
|
102
|
+
const { components, reducers } = resolvedBundle;
|
|
91
103
|
const component = components[pageComponent];
|
|
92
104
|
// pageComponent should have required fields even if it is lazy,
|
|
93
105
|
// thanks to `git log 9466cb32bfb71ba49144ef839d3d5bce246e213c -L90,103:packages/modules/common/src/bundleManager/bundleManager.ts`
|
|
94
106
|
(_a = component === null || component === void 0 ? void 0 : component.reducers) === null || _a === void 0 ? void 0 : _a.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
95
107
|
reducers === null || reducers === void 0 ? void 0 : reducers.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
108
|
+
// @todo: reuse logic from bundleManager?
|
|
109
|
+
// register nested layout actions and reducers for current page
|
|
110
|
+
if (resolvedLayout) {
|
|
111
|
+
if ('actions' in resolvedLayout && isArray(resolvedLayout.actions)) {
|
|
112
|
+
actionRegistry.add(pageComponent, resolvedLayout.actions);
|
|
113
|
+
}
|
|
114
|
+
if ('reducers' in resolvedLayout && isArray(resolvedLayout.reducers)) {
|
|
115
|
+
resolvedLayout.reducers.forEach((reducer) => {
|
|
116
|
+
dispatcher.registerStore(reducer);
|
|
117
|
+
dispatcherContext.getStore(reducer);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
96
121
|
}
|
|
97
122
|
catch (error) {
|
|
98
123
|
log.error({
|
|
@@ -128,7 +153,9 @@ const commonGuards = [
|
|
|
128
153
|
bundleManager: BUNDLE_MANAGER_TOKEN,
|
|
129
154
|
actionRegistry: ACTION_REGISTRY_TOKEN,
|
|
130
155
|
responseManager: RESPONSE_MANAGER_TOKEN,
|
|
156
|
+
dispatcher: DISPATCHER_TOKEN,
|
|
131
157
|
dispatcherContext: DISPATCHER_CONTEXT_TOKEN,
|
|
158
|
+
pageService: PAGE_SERVICE_TOKEN,
|
|
132
159
|
},
|
|
133
160
|
},
|
|
134
161
|
{
|
|
@@ -430,6 +457,111 @@ const providers$1 = [
|
|
|
430
457
|
},
|
|
431
458
|
];
|
|
432
459
|
|
|
460
|
+
const PREFETCHED_LINKS_CACHE_TOKEN = createToken$1();
|
|
461
|
+
const PREFETCHED_LINKS_QUEUE_TOKEN = createToken$1();
|
|
462
|
+
const prefetchProviders = [
|
|
463
|
+
provide({
|
|
464
|
+
provide: PREFETCHED_LINKS_CACHE_TOKEN,
|
|
465
|
+
useValue: new Set(),
|
|
466
|
+
}),
|
|
467
|
+
provide({
|
|
468
|
+
provide: PREFETCHED_LINKS_QUEUE_TOKEN,
|
|
469
|
+
useFactory: () => {
|
|
470
|
+
let queue = Promise.resolve();
|
|
471
|
+
return {
|
|
472
|
+
add(run) {
|
|
473
|
+
queue = queue.then(run);
|
|
474
|
+
return queue;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
provide({
|
|
480
|
+
provide: LINK_PREFETCH_MANAGER_TOKEN,
|
|
481
|
+
useFactory: ({ router, routeTransform, routeResolve, componentRegistry, bundleManager, logger, prefetchedLinksCache, prefetchedLinksQueue, }) => {
|
|
482
|
+
const log = logger('link-prefetch-manager');
|
|
483
|
+
const prefetch = async (url) => {
|
|
484
|
+
// prefetch needed for cache assets by browser, so we want to prefetch resources for any page only once.
|
|
485
|
+
// cache will work only for SPA-router, but anyway resources loading deduplication logic already included in @loadable
|
|
486
|
+
if (prefetchedLinksCache.has(url)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
prefetchedLinksCache.add(url);
|
|
490
|
+
log.info({ event: 'prefetch-route-init', url });
|
|
491
|
+
// first, try to find static or resolved dynamic route
|
|
492
|
+
let route = router.resolve(url);
|
|
493
|
+
// if route not found, try to resolve dynamic route,
|
|
494
|
+
// logic from `ROUTER_TOKEN` provider factory, without `router.addRoute` method call
|
|
495
|
+
if (!route && routeResolve) {
|
|
496
|
+
// add query `prefetchRoute` to indicate to any `routeResolve` implementation
|
|
497
|
+
// that this route request no need to be cached or have any personalization
|
|
498
|
+
const parsedUrl = addQuery(url, { prefetchRoute: 'true' });
|
|
499
|
+
route = await routeResolve({
|
|
500
|
+
url: parsedUrl,
|
|
501
|
+
type: 'navigate',
|
|
502
|
+
});
|
|
503
|
+
if (route) {
|
|
504
|
+
route = routeTransform(route);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!route) {
|
|
508
|
+
log.info({ event: 'prefetch-route-not-found', url });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// @todo: очередь запросов!
|
|
512
|
+
// get name of everything what we need to prefetch,
|
|
513
|
+
// @loadable will load all scripts and styles for us
|
|
514
|
+
const { config: { pageComponent, bundle, nestedLayoutComponent }, } = route;
|
|
515
|
+
const promises = [];
|
|
516
|
+
// no need for bundle and page component actions amd reducers initialization,
|
|
517
|
+
// so copy only part of `modules/guards/common/loadBundle.ts` logic,
|
|
518
|
+
// all relative assets will be fetched
|
|
519
|
+
if (bundleManager.has(bundle, pageComponent)) {
|
|
520
|
+
promises.push(bundleManager.get(bundle, pageComponent));
|
|
521
|
+
}
|
|
522
|
+
// no need for nestedLayoutComponent actions amd reducers initialization,
|
|
523
|
+
// so copy only part of `modules/guards/common/loadBundle.ts` logic,
|
|
524
|
+
// all relative assets will be fetched
|
|
525
|
+
if (nestedLayoutComponent) {
|
|
526
|
+
promises.push(resolveLazyComponent(componentRegistry.get(nestedLayoutComponent, isFileSystemPageComponent(pageComponent) ? '__default' : bundle)));
|
|
527
|
+
}
|
|
528
|
+
await Promise.all(promises)
|
|
529
|
+
.then(() => {
|
|
530
|
+
log.info({ event: 'prefetch-route-success', url });
|
|
531
|
+
})
|
|
532
|
+
.catch((error) => {
|
|
533
|
+
log.warn({
|
|
534
|
+
event: 'prefetch-fail',
|
|
535
|
+
url,
|
|
536
|
+
error,
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
prefetch: (url) => {
|
|
542
|
+
// fetch assets sequentially
|
|
543
|
+
return prefetchedLinksQueue.add(() => prefetch(url));
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
deps: {
|
|
548
|
+
router: ROUTER_TOKEN,
|
|
549
|
+
routeTransform: routeTransformToken,
|
|
550
|
+
routeResolve: {
|
|
551
|
+
token: ROUTE_RESOLVE_TOKEN,
|
|
552
|
+
optional: true,
|
|
553
|
+
},
|
|
554
|
+
componentRegistry: COMPONENT_REGISTRY_TOKEN,
|
|
555
|
+
bundleManager: BUNDLE_MANAGER_TOKEN,
|
|
556
|
+
logger: LOGGER_TOKEN,
|
|
557
|
+
prefetchedLinksCache: PREFETCHED_LINKS_CACHE_TOKEN,
|
|
558
|
+
prefetchedLinksQueue: PREFETCHED_LINKS_QUEUE_TOKEN,
|
|
559
|
+
},
|
|
560
|
+
}),
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
const clientTokens = [...prefetchProviders];
|
|
564
|
+
|
|
433
565
|
const stopRunAtError = (error) => {
|
|
434
566
|
if (isNotFoundError(error) || isRedirectFoundError(error)) {
|
|
435
567
|
return true;
|
|
@@ -461,6 +593,7 @@ const runActionsFactory = ({ store, router, actionRegistry, actionPageRunner, })
|
|
|
461
593
|
};
|
|
462
594
|
|
|
463
595
|
const providers = [
|
|
596
|
+
...clientTokens,
|
|
464
597
|
...providers$1,
|
|
465
598
|
provide({
|
|
466
599
|
provide: commandLineListTokens.customerStart,
|
|
@@ -529,7 +662,9 @@ const providers = [
|
|
|
529
662
|
// run navigation to current location.
|
|
530
663
|
if (renderMode === 'client' &&
|
|
531
664
|
(!currentRoute || (currentRoute && currentRoute.actualPath !== window.location.pathname))) {
|
|
532
|
-
|
|
665
|
+
// replace because otherwice we will push in the history the same url twice,
|
|
666
|
+
// and history.back will return to the same url
|
|
667
|
+
return router.navigate({ url: window.location.href, replace: true });
|
|
533
668
|
}
|
|
534
669
|
};
|
|
535
670
|
},
|
|
@@ -683,4 +818,102 @@ RouterChildAppModule = __decorate([
|
|
|
683
818
|
})
|
|
684
819
|
], RouterChildAppModule);
|
|
685
820
|
|
|
686
|
-
|
|
821
|
+
const requestIdleCallback = window.requestIdleCallback ||
|
|
822
|
+
function (callback) {
|
|
823
|
+
return window.setTimeout(callback, 1);
|
|
824
|
+
};
|
|
825
|
+
const cancelIdleCallback = window.cancelIdleCallback ||
|
|
826
|
+
function (timer) {
|
|
827
|
+
return window.clearTimeout(timer);
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
const isUserNetworkConditionsSuitableForPrefetch = () => {
|
|
831
|
+
var _a;
|
|
832
|
+
const { saveData, effectiveType = '' } = (_a = navigator.connection) !== null && _a !== void 0 ? _a : {};
|
|
833
|
+
return !saveData && !(effectiveType === '2g' || effectiveType === 'slow-2g');
|
|
834
|
+
};
|
|
835
|
+
const usePrefetch = ({ url, target, prefetch, }) => {
|
|
836
|
+
const linkPrefetchManager = useDi(LINK_PREFETCH_MANAGER_TOKEN);
|
|
837
|
+
useEffect(() => {
|
|
838
|
+
if (!target || !prefetch) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
let idleId = null;
|
|
842
|
+
const observer = new IntersectionObserver((entries) => {
|
|
843
|
+
entries.forEach((entry) => {
|
|
844
|
+
// when `Link` element in viewport
|
|
845
|
+
if (entry.isIntersecting && entry.intersectionRatio > 0) {
|
|
846
|
+
// only once
|
|
847
|
+
observer.disconnect();
|
|
848
|
+
if (isUserNetworkConditionsSuitableForPrefetch()) {
|
|
849
|
+
// trigger prefetching when browser is idle
|
|
850
|
+
idleId = requestIdleCallback(() => {
|
|
851
|
+
linkPrefetchManager.prefetch(url);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
observer.observe(target);
|
|
858
|
+
return () => {
|
|
859
|
+
observer.disconnect();
|
|
860
|
+
cancelIdleCallback(idleId);
|
|
861
|
+
};
|
|
862
|
+
}, [url, target, prefetch, linkPrefetchManager]);
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
function Link(props) {
|
|
866
|
+
const { children, onClick, url, query, replace, target, navigateOptions, prefetch = true, ...otherProps } = props;
|
|
867
|
+
const navigate = useNavigate({ url, query, replace, ...navigateOptions });
|
|
868
|
+
const [linkElement, setLinkElement] = useState(null);
|
|
869
|
+
usePrefetch({
|
|
870
|
+
url,
|
|
871
|
+
prefetch,
|
|
872
|
+
target: linkElement,
|
|
873
|
+
});
|
|
874
|
+
const handleClick = useCallback((event) => {
|
|
875
|
+
// ignores the navigation when clicked using right mouse button or
|
|
876
|
+
// by holding a special modifier key: ctrl, command, win, alt, shift
|
|
877
|
+
if (target ||
|
|
878
|
+
event.ctrlKey ||
|
|
879
|
+
event.metaKey ||
|
|
880
|
+
event.altKey ||
|
|
881
|
+
event.shiftKey ||
|
|
882
|
+
event.button !== 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
event.preventDefault();
|
|
886
|
+
navigate();
|
|
887
|
+
onClick && onClick(event);
|
|
888
|
+
}, [navigate, target, onClick]);
|
|
889
|
+
const extraProps = { href: url, onClick: handleClick, target };
|
|
890
|
+
if (isValidElement(children)) {
|
|
891
|
+
return cloneElement(children, {
|
|
892
|
+
// @ts-expect-error
|
|
893
|
+
ref: (element) => {
|
|
894
|
+
// @ts-expect-error
|
|
895
|
+
const { ref } = children;
|
|
896
|
+
// preserve original ref
|
|
897
|
+
if (typeof ref === 'function') {
|
|
898
|
+
ref(element);
|
|
899
|
+
}
|
|
900
|
+
else if (typeof ref === 'object' && ref !== null) {
|
|
901
|
+
ref.current = element;
|
|
902
|
+
}
|
|
903
|
+
setLinkElement(element);
|
|
904
|
+
},
|
|
905
|
+
...extraProps,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return (
|
|
909
|
+
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
910
|
+
jsx("a", { ref: (element) => setLinkElement(element), ...otherProps, ...extraProps, children: children }));
|
|
911
|
+
}
|
|
912
|
+
Link.displayName = 'Link';
|
|
913
|
+
|
|
914
|
+
const usePageService = () => {
|
|
915
|
+
useRoute();
|
|
916
|
+
return useDi(PAGE_SERVICE_TOKEN);
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
export { Link, NoSpaRouterModule, RouterChildAppModule, RouterStore, SpaRouterModule, generateForRoot, setCurrentNavigation, setUrlOnRehydrate, usePageService };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
export { NoSpaRouterModule, SpaRouterModule } from './modules/server';
|
|
2
2
|
export { RouterChildAppModule } from './modules/child-app';
|
|
3
|
-
export { Provider, useNavigate, useRoute, useRouter, useUrl
|
|
3
|
+
export { Provider, useNavigate, useRoute, useRouter, useUrl } from '@tinkoff/router';
|
|
4
|
+
export { Link } from './components/link';
|
|
4
5
|
export * from '@tramvai/tokens-router';
|
|
5
6
|
export { generateForRoot } from './modules/utils/forRoot';
|
|
6
7
|
export * from './stores/RouterStore';
|
|
8
|
+
export * from './hooks/usePageService';
|
|
9
|
+
type RouteConfig = {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
bundle?: string;
|
|
12
|
+
pageComponent?: string;
|
|
13
|
+
layoutComponent?: string;
|
|
14
|
+
nestedLayoutComponent?: string;
|
|
15
|
+
errorBoundaryComponent?: string;
|
|
16
|
+
meta?: {
|
|
17
|
+
seo?: {
|
|
18
|
+
metaTags?: Record<string, any>;
|
|
19
|
+
shareSchema?: Record<string, any>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
declare module '@tramvai/react' {
|
|
24
|
+
interface PageComponentOptions {
|
|
25
|
+
routeConfig?: RouteConfig;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/index.es.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
2
|
import { provide, commandLineListTokens, Module } from '@tramvai/core';
|
|
3
|
-
import { Provider, setLogger, isWildcard, isHistoryFallback, Router } from '@tinkoff/router';
|
|
4
|
-
export {
|
|
5
|
-
import { LOGGER_TOKEN, BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_CONTEXT_TOKEN, CONTEXT_TOKEN, COMPONENT_REGISTRY_TOKEN, COMBINE_REDUCERS, REQUEST_MANAGER_TOKEN, STORE_TOKEN, ACTION_PAGE_RUNNER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
-
import { ROUTER_GUARD_TOKEN, ROUTE_TRANSFORM_TOKEN, ROUTER_TOKEN, ROUTES_TOKEN, ROUTE_RESOLVE_TOKEN, ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN,
|
|
3
|
+
import { Provider, setLogger, isWildcard, isHistoryFallback, Router, useNavigate, useRoute } from '@tinkoff/router';
|
|
4
|
+
export { Provider, useNavigate, useRoute, useRouter, useUrl } from '@tinkoff/router';
|
|
5
|
+
import { LOGGER_TOKEN, BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_TOKEN, DISPATCHER_CONTEXT_TOKEN, CONTEXT_TOKEN, COMPONENT_REGISTRY_TOKEN, COMBINE_REDUCERS, REQUEST_MANAGER_TOKEN, STORE_TOKEN, ACTION_PAGE_RUNNER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
+
import { ROUTER_GUARD_TOKEN, PAGE_SERVICE_TOKEN, ROUTE_TRANSFORM_TOKEN, ROUTER_TOKEN, ROUTES_TOKEN, ROUTE_RESOLVE_TOKEN, ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN, LINK_PREFETCH_MANAGER_TOKEN } from '@tramvai/tokens-router';
|
|
7
7
|
export * from '@tramvai/tokens-router';
|
|
8
8
|
import flatten from '@tinkoff/utils/array/flatten';
|
|
9
9
|
import { createToken } from '@tinkoff/dippy';
|
|
10
10
|
import { createEvent, createReducer } from '@tramvai/state';
|
|
11
|
+
import isArray from '@tinkoff/utils/is/array';
|
|
12
|
+
import { resolveLazyComponent, useDi } from '@tramvai/react';
|
|
11
13
|
import { EXTEND_RENDER } from '@tramvai/tokens-render';
|
|
12
14
|
import identity from '@tinkoff/utils/function/identity';
|
|
13
15
|
import compose from '@tinkoff/utils/function/compose';
|
|
@@ -20,6 +22,7 @@ import { SERVER_MODULE_PAPI_PUBLIC_ROUTE } from '@tramvai/tokens-server';
|
|
|
20
22
|
import prop from '@tinkoff/utils/object/prop';
|
|
21
23
|
import { createPapiMethod } from '@tramvai/papi';
|
|
22
24
|
import { CHILD_APP_INTERNAL_ROOT_STATE_ALLOWED_STORE_TOKEN } from '@tramvai/tokens-child-app';
|
|
25
|
+
import { useState, useCallback, isValidElement, cloneElement } from 'react';
|
|
23
26
|
|
|
24
27
|
const routerClassToken = createToken('router routerClassToken');
|
|
25
28
|
const additionalRouterParameters = createToken('router additionalParameters');
|
|
@@ -66,7 +69,7 @@ const RouterStore = createReducer('router', initialState)
|
|
|
66
69
|
};
|
|
67
70
|
});
|
|
68
71
|
|
|
69
|
-
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcherContext, }) => {
|
|
72
|
+
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcher, dispatcherContext, pageService, }) => {
|
|
70
73
|
const log = logger('route:load-bundles');
|
|
71
74
|
return async ({ to }) => {
|
|
72
75
|
var _a;
|
|
@@ -74,7 +77,7 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
74
77
|
event: 'load-bundle',
|
|
75
78
|
route: to,
|
|
76
79
|
});
|
|
77
|
-
const { bundle, pageComponent } = to.config;
|
|
80
|
+
const { bundle, pageComponent, nestedLayoutComponent } = to.config;
|
|
78
81
|
if (!bundleManager.has(bundle, pageComponent)) {
|
|
79
82
|
log.info({
|
|
80
83
|
event: 'load-bundle-not-found',
|
|
@@ -89,12 +92,33 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
89
92
|
// несмотря на наличие бандла в разметке, все равно мы должны дождаться
|
|
90
93
|
// его загрузки перед рендером
|
|
91
94
|
try {
|
|
92
|
-
const
|
|
95
|
+
const promises = [];
|
|
96
|
+
// for file-system pages bundle is virtual, so it will be loaded in parallel with nested layout
|
|
97
|
+
// for classic bundles, page component will be loaded after bundle loading
|
|
98
|
+
promises.push(bundleManager.get(bundle, pageComponent));
|
|
99
|
+
if (nestedLayoutComponent) {
|
|
100
|
+
promises.push(resolveLazyComponent(pageService.resolveComponentFromConfig('nestedLayout')));
|
|
101
|
+
}
|
|
102
|
+
const [resolvedBundle, resolvedLayout] = await Promise.all(promises);
|
|
103
|
+
const { components, reducers } = resolvedBundle;
|
|
93
104
|
const component = components[pageComponent];
|
|
94
105
|
// pageComponent should have required fields even if it is lazy,
|
|
95
106
|
// thanks to `git log 9466cb32bfb71ba49144ef839d3d5bce246e213c -L90,103:packages/modules/common/src/bundleManager/bundleManager.ts`
|
|
96
107
|
(_a = component === null || component === void 0 ? void 0 : component.reducers) === null || _a === void 0 ? void 0 : _a.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
97
108
|
reducers === null || reducers === void 0 ? void 0 : reducers.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
109
|
+
// @todo: reuse logic from bundleManager?
|
|
110
|
+
// register nested layout actions and reducers for current page
|
|
111
|
+
if (resolvedLayout) {
|
|
112
|
+
if ('actions' in resolvedLayout && isArray(resolvedLayout.actions)) {
|
|
113
|
+
actionRegistry.add(pageComponent, resolvedLayout.actions);
|
|
114
|
+
}
|
|
115
|
+
if ('reducers' in resolvedLayout && isArray(resolvedLayout.reducers)) {
|
|
116
|
+
resolvedLayout.reducers.forEach((reducer) => {
|
|
117
|
+
dispatcher.registerStore(reducer);
|
|
118
|
+
dispatcherContext.getStore(reducer);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
98
122
|
}
|
|
99
123
|
catch (error) {
|
|
100
124
|
log.error({
|
|
@@ -130,7 +154,9 @@ const commonGuards = [
|
|
|
130
154
|
bundleManager: BUNDLE_MANAGER_TOKEN,
|
|
131
155
|
actionRegistry: ACTION_REGISTRY_TOKEN,
|
|
132
156
|
responseManager: RESPONSE_MANAGER_TOKEN,
|
|
157
|
+
dispatcher: DISPATCHER_TOKEN,
|
|
133
158
|
dispatcherContext: DISPATCHER_CONTEXT_TOKEN,
|
|
159
|
+
pageService: PAGE_SERVICE_TOKEN,
|
|
134
160
|
},
|
|
135
161
|
},
|
|
136
162
|
{
|
|
@@ -573,6 +599,17 @@ const bundleInfoPapi = createPapiMethod({
|
|
|
573
599
|
},
|
|
574
600
|
});
|
|
575
601
|
|
|
602
|
+
// do nothing on server side
|
|
603
|
+
const prefetchManager = {
|
|
604
|
+
prefetch: async (url) => { },
|
|
605
|
+
};
|
|
606
|
+
const prefetchProviders = [
|
|
607
|
+
provide({
|
|
608
|
+
provide: LINK_PREFETCH_MANAGER_TOKEN,
|
|
609
|
+
useValue: prefetchManager,
|
|
610
|
+
}),
|
|
611
|
+
];
|
|
612
|
+
|
|
576
613
|
const serverTokens = [
|
|
577
614
|
{
|
|
578
615
|
provide: additionalRouterParameters,
|
|
@@ -587,6 +624,7 @@ const serverTokens = [
|
|
|
587
624
|
multi: true,
|
|
588
625
|
useValue: bundleInfoPapi,
|
|
589
626
|
},
|
|
627
|
+
...prefetchProviders,
|
|
590
628
|
];
|
|
591
629
|
|
|
592
630
|
const generateForRoot = (mainModule) => {
|
|
@@ -675,4 +713,53 @@ RouterChildAppModule = __decorate([
|
|
|
675
713
|
})
|
|
676
714
|
], RouterChildAppModule);
|
|
677
715
|
|
|
678
|
-
|
|
716
|
+
function Link(props) {
|
|
717
|
+
const { children, onClick, url, query, replace, target, navigateOptions, prefetch = true, ...otherProps } = props;
|
|
718
|
+
const navigate = useNavigate({ url, query, replace, ...navigateOptions });
|
|
719
|
+
const [linkElement, setLinkElement] = useState(null);
|
|
720
|
+
const handleClick = useCallback((event) => {
|
|
721
|
+
// ignores the navigation when clicked using right mouse button or
|
|
722
|
+
// by holding a special modifier key: ctrl, command, win, alt, shift
|
|
723
|
+
if (target ||
|
|
724
|
+
event.ctrlKey ||
|
|
725
|
+
event.metaKey ||
|
|
726
|
+
event.altKey ||
|
|
727
|
+
event.shiftKey ||
|
|
728
|
+
event.button !== 0) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
navigate();
|
|
733
|
+
onClick && onClick(event);
|
|
734
|
+
}, [navigate, target, onClick]);
|
|
735
|
+
const extraProps = { href: url, onClick: handleClick, target };
|
|
736
|
+
if (isValidElement(children)) {
|
|
737
|
+
return cloneElement(children, {
|
|
738
|
+
// @ts-expect-error
|
|
739
|
+
ref: (element) => {
|
|
740
|
+
// @ts-expect-error
|
|
741
|
+
const { ref } = children;
|
|
742
|
+
// preserve original ref
|
|
743
|
+
if (typeof ref === 'function') {
|
|
744
|
+
ref(element);
|
|
745
|
+
}
|
|
746
|
+
else if (typeof ref === 'object' && ref !== null) {
|
|
747
|
+
ref.current = element;
|
|
748
|
+
}
|
|
749
|
+
setLinkElement(element);
|
|
750
|
+
},
|
|
751
|
+
...extraProps,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return (
|
|
755
|
+
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
756
|
+
jsx("a", { ref: (element) => setLinkElement(element), ...otherProps, ...extraProps, children: children }));
|
|
757
|
+
}
|
|
758
|
+
Link.displayName = 'Link';
|
|
759
|
+
|
|
760
|
+
const usePageService = () => {
|
|
761
|
+
useRoute();
|
|
762
|
+
return useDi(PAGE_SERVICE_TOKEN);
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
export { Link, NoSpaRouterModule, RouterChildAppModule, RouterStore, SpaRouterModule, generateForRoot, setCurrentNavigation, setUrlOnRehydrate, usePageService };
|
package/lib/index.js
CHANGED
|
@@ -10,6 +10,8 @@ var tokensRouter = require('@tramvai/tokens-router');
|
|
|
10
10
|
var flatten = require('@tinkoff/utils/array/flatten');
|
|
11
11
|
var dippy = require('@tinkoff/dippy');
|
|
12
12
|
var state = require('@tramvai/state');
|
|
13
|
+
var isArray = require('@tinkoff/utils/is/array');
|
|
14
|
+
var react = require('@tramvai/react');
|
|
13
15
|
var tokensRender = require('@tramvai/tokens-render');
|
|
14
16
|
var identity = require('@tinkoff/utils/function/identity');
|
|
15
17
|
var compose = require('@tinkoff/utils/function/compose');
|
|
@@ -22,10 +24,12 @@ var tokensServer = require('@tramvai/tokens-server');
|
|
|
22
24
|
var prop = require('@tinkoff/utils/object/prop');
|
|
23
25
|
var papi = require('@tramvai/papi');
|
|
24
26
|
var tokensChildApp = require('@tramvai/tokens-child-app');
|
|
27
|
+
var react$1 = require('react');
|
|
25
28
|
|
|
26
29
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
27
30
|
|
|
28
31
|
var flatten__default = /*#__PURE__*/_interopDefaultLegacy(flatten);
|
|
32
|
+
var isArray__default = /*#__PURE__*/_interopDefaultLegacy(isArray);
|
|
29
33
|
var identity__default = /*#__PURE__*/_interopDefaultLegacy(identity);
|
|
30
34
|
var compose__default = /*#__PURE__*/_interopDefaultLegacy(compose);
|
|
31
35
|
var replace__default = /*#__PURE__*/_interopDefaultLegacy(replace);
|
|
@@ -77,7 +81,7 @@ const RouterStore = state.createReducer('router', initialState)
|
|
|
77
81
|
};
|
|
78
82
|
});
|
|
79
83
|
|
|
80
|
-
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcherContext, }) => {
|
|
84
|
+
const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, dispatcher, dispatcherContext, pageService, }) => {
|
|
81
85
|
const log = logger('route:load-bundles');
|
|
82
86
|
return async ({ to }) => {
|
|
83
87
|
var _a;
|
|
@@ -85,7 +89,7 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
85
89
|
event: 'load-bundle',
|
|
86
90
|
route: to,
|
|
87
91
|
});
|
|
88
|
-
const { bundle, pageComponent } = to.config;
|
|
92
|
+
const { bundle, pageComponent, nestedLayoutComponent } = to.config;
|
|
89
93
|
if (!bundleManager.has(bundle, pageComponent)) {
|
|
90
94
|
log.info({
|
|
91
95
|
event: 'load-bundle-not-found',
|
|
@@ -100,12 +104,33 @@ const loadBundle = ({ bundleManager, logger, actionRegistry, responseManager, di
|
|
|
100
104
|
// несмотря на наличие бандла в разметке, все равно мы должны дождаться
|
|
101
105
|
// его загрузки перед рендером
|
|
102
106
|
try {
|
|
103
|
-
const
|
|
107
|
+
const promises = [];
|
|
108
|
+
// for file-system pages bundle is virtual, so it will be loaded in parallel with nested layout
|
|
109
|
+
// for classic bundles, page component will be loaded after bundle loading
|
|
110
|
+
promises.push(bundleManager.get(bundle, pageComponent));
|
|
111
|
+
if (nestedLayoutComponent) {
|
|
112
|
+
promises.push(react.resolveLazyComponent(pageService.resolveComponentFromConfig('nestedLayout')));
|
|
113
|
+
}
|
|
114
|
+
const [resolvedBundle, resolvedLayout] = await Promise.all(promises);
|
|
115
|
+
const { components, reducers } = resolvedBundle;
|
|
104
116
|
const component = components[pageComponent];
|
|
105
117
|
// pageComponent should have required fields even if it is lazy,
|
|
106
118
|
// thanks to `git log 9466cb32bfb71ba49144ef839d3d5bce246e213c -L90,103:packages/modules/common/src/bundleManager/bundleManager.ts`
|
|
107
119
|
(_a = component === null || component === void 0 ? void 0 : component.reducers) === null || _a === void 0 ? void 0 : _a.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
108
120
|
reducers === null || reducers === void 0 ? void 0 : reducers.forEach((reducer) => dispatcherContext.getStore(reducer));
|
|
121
|
+
// @todo: reuse logic from bundleManager?
|
|
122
|
+
// register nested layout actions and reducers for current page
|
|
123
|
+
if (resolvedLayout) {
|
|
124
|
+
if ('actions' in resolvedLayout && isArray__default["default"](resolvedLayout.actions)) {
|
|
125
|
+
actionRegistry.add(pageComponent, resolvedLayout.actions);
|
|
126
|
+
}
|
|
127
|
+
if ('reducers' in resolvedLayout && isArray__default["default"](resolvedLayout.reducers)) {
|
|
128
|
+
resolvedLayout.reducers.forEach((reducer) => {
|
|
129
|
+
dispatcher.registerStore(reducer);
|
|
130
|
+
dispatcherContext.getStore(reducer);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
109
134
|
}
|
|
110
135
|
catch (error) {
|
|
111
136
|
log.error({
|
|
@@ -141,7 +166,9 @@ const commonGuards = [
|
|
|
141
166
|
bundleManager: tokensCommon.BUNDLE_MANAGER_TOKEN,
|
|
142
167
|
actionRegistry: tokensCommon.ACTION_REGISTRY_TOKEN,
|
|
143
168
|
responseManager: tokensCommon.RESPONSE_MANAGER_TOKEN,
|
|
169
|
+
dispatcher: tokensCommon.DISPATCHER_TOKEN,
|
|
144
170
|
dispatcherContext: tokensCommon.DISPATCHER_CONTEXT_TOKEN,
|
|
171
|
+
pageService: tokensRouter.PAGE_SERVICE_TOKEN,
|
|
145
172
|
},
|
|
146
173
|
},
|
|
147
174
|
{
|
|
@@ -584,6 +611,17 @@ const bundleInfoPapi = papi.createPapiMethod({
|
|
|
584
611
|
},
|
|
585
612
|
});
|
|
586
613
|
|
|
614
|
+
// do nothing on server side
|
|
615
|
+
const prefetchManager = {
|
|
616
|
+
prefetch: async (url) => { },
|
|
617
|
+
};
|
|
618
|
+
const prefetchProviders = [
|
|
619
|
+
core.provide({
|
|
620
|
+
provide: tokensRouter.LINK_PREFETCH_MANAGER_TOKEN,
|
|
621
|
+
useValue: prefetchManager,
|
|
622
|
+
}),
|
|
623
|
+
];
|
|
624
|
+
|
|
587
625
|
const serverTokens = [
|
|
588
626
|
{
|
|
589
627
|
provide: additionalRouterParameters,
|
|
@@ -598,6 +636,7 @@ const serverTokens = [
|
|
|
598
636
|
multi: true,
|
|
599
637
|
useValue: bundleInfoPapi,
|
|
600
638
|
},
|
|
639
|
+
...prefetchProviders,
|
|
601
640
|
];
|
|
602
641
|
|
|
603
642
|
const generateForRoot = (mainModule) => {
|
|
@@ -686,10 +725,55 @@ exports.RouterChildAppModule = tslib.__decorate([
|
|
|
686
725
|
})
|
|
687
726
|
], exports.RouterChildAppModule);
|
|
688
727
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
728
|
+
function Link(props) {
|
|
729
|
+
const { children, onClick, url, query, replace, target, navigateOptions, prefetch = true, ...otherProps } = props;
|
|
730
|
+
const navigate = router.useNavigate({ url, query, replace, ...navigateOptions });
|
|
731
|
+
const [linkElement, setLinkElement] = react$1.useState(null);
|
|
732
|
+
const handleClick = react$1.useCallback((event) => {
|
|
733
|
+
// ignores the navigation when clicked using right mouse button or
|
|
734
|
+
// by holding a special modifier key: ctrl, command, win, alt, shift
|
|
735
|
+
if (target ||
|
|
736
|
+
event.ctrlKey ||
|
|
737
|
+
event.metaKey ||
|
|
738
|
+
event.altKey ||
|
|
739
|
+
event.shiftKey ||
|
|
740
|
+
event.button !== 0) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
event.preventDefault();
|
|
744
|
+
navigate();
|
|
745
|
+
onClick && onClick(event);
|
|
746
|
+
}, [navigate, target, onClick]);
|
|
747
|
+
const extraProps = { href: url, onClick: handleClick, target };
|
|
748
|
+
if (react$1.isValidElement(children)) {
|
|
749
|
+
return react$1.cloneElement(children, {
|
|
750
|
+
// @ts-expect-error
|
|
751
|
+
ref: (element) => {
|
|
752
|
+
// @ts-expect-error
|
|
753
|
+
const { ref } = children;
|
|
754
|
+
// preserve original ref
|
|
755
|
+
if (typeof ref === 'function') {
|
|
756
|
+
ref(element);
|
|
757
|
+
}
|
|
758
|
+
else if (typeof ref === 'object' && ref !== null) {
|
|
759
|
+
ref.current = element;
|
|
760
|
+
}
|
|
761
|
+
setLinkElement(element);
|
|
762
|
+
},
|
|
763
|
+
...extraProps,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
return (
|
|
767
|
+
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
768
|
+
jsxRuntime.jsx("a", { ref: (element) => setLinkElement(element), ...otherProps, ...extraProps, children: children }));
|
|
769
|
+
}
|
|
770
|
+
Link.displayName = 'Link';
|
|
771
|
+
|
|
772
|
+
const usePageService = () => {
|
|
773
|
+
router.useRoute();
|
|
774
|
+
return react.useDi(tokensRouter.PAGE_SERVICE_TOKEN);
|
|
775
|
+
};
|
|
776
|
+
|
|
693
777
|
Object.defineProperty(exports, 'Provider', {
|
|
694
778
|
enumerable: true,
|
|
695
779
|
get: function () { return router.Provider; }
|
|
@@ -710,11 +794,13 @@ Object.defineProperty(exports, 'useUrl', {
|
|
|
710
794
|
enumerable: true,
|
|
711
795
|
get: function () { return router.useUrl; }
|
|
712
796
|
});
|
|
797
|
+
exports.Link = Link;
|
|
713
798
|
exports.RouterStore = RouterStore;
|
|
714
799
|
exports.SpaRouterModule = SpaRouterModule;
|
|
715
800
|
exports.generateForRoot = generateForRoot;
|
|
716
801
|
exports.setCurrentNavigation = setCurrentNavigation;
|
|
717
802
|
exports.setUrlOnRehydrate = setUrlOnRehydrate;
|
|
803
|
+
exports.usePageService = usePageService;
|
|
718
804
|
Object.keys(tokensRouter).forEach(function (k) {
|
|
719
805
|
if (k !== 'default' && !exports.hasOwnProperty(k)) Object.defineProperty(exports, k, {
|
|
720
806
|
enumerable: true,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { NavigationGuard } from '@tinkoff/router';
|
|
2
|
-
import type { BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, LOGGER_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_CONTEXT_TOKEN } from '@tramvai/tokens-common';
|
|
3
|
-
|
|
2
|
+
import type { BUNDLE_MANAGER_TOKEN, ACTION_REGISTRY_TOKEN, LOGGER_TOKEN, RESPONSE_MANAGER_TOKEN, DISPATCHER_CONTEXT_TOKEN, DISPATCHER_TOKEN } from '@tramvai/tokens-common';
|
|
3
|
+
import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
|
|
4
|
+
export declare const loadBundle: ({ bundleManager, logger, actionRegistry, responseManager, dispatcher, dispatcherContext, pageService, }: {
|
|
4
5
|
bundleManager: typeof BUNDLE_MANAGER_TOKEN;
|
|
5
6
|
logger: typeof LOGGER_TOKEN;
|
|
6
7
|
actionRegistry: typeof ACTION_REGISTRY_TOKEN;
|
|
7
8
|
responseManager: typeof RESPONSE_MANAGER_TOKEN;
|
|
9
|
+
dispatcher: typeof DISPATCHER_TOKEN;
|
|
8
10
|
dispatcherContext: typeof DISPATCHER_CONTEXT_TOKEN;
|
|
11
|
+
pageService: typeof PAGE_SERVICE_TOKEN;
|
|
9
12
|
}) => NavigationGuard;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const clientTokens: (import("@tinkoff/dippy").Provider<unknown, Set<string>> | import("@tinkoff/dippy").Provider<unknown, {
|
|
2
|
+
add(run: () => Promise<void>): Promise<void>;
|
|
3
|
+
}> | import("@tinkoff/dippy").Provider<{
|
|
4
|
+
router: import("@tinkoff/dippy").BaseTokenInterface<import("@tinkoff/router").AbstractRouter>;
|
|
5
|
+
routeTransform: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-router").RouteTransform>;
|
|
6
|
+
routeResolve: {
|
|
7
|
+
token: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-router").RouteResolve>;
|
|
8
|
+
optional: boolean;
|
|
9
|
+
};
|
|
10
|
+
componentRegistry: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").ComponentRegistry>;
|
|
11
|
+
bundleManager: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").BundleManager>;
|
|
12
|
+
logger: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").LoggerFactory>;
|
|
13
|
+
prefetchedLinksCache: import("@tinkoff/dippy").BaseTokenInterface<Set<string>>;
|
|
14
|
+
prefetchedLinksQueue: import("@tinkoff/dippy").BaseTokenInterface<{
|
|
15
|
+
add(run: () => Promise<void>): Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
}, {
|
|
18
|
+
prefetch: (url: string) => Promise<void>;
|
|
19
|
+
}>)[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const prefetchProviders: (import("@tramvai/core").Provider<unknown, Set<string>> | import("@tramvai/core").Provider<unknown, {
|
|
2
|
+
add(run: () => Promise<void>): Promise<void>;
|
|
3
|
+
}> | import("@tramvai/core").Provider<{
|
|
4
|
+
router: import("@tinkoff/dippy").BaseTokenInterface<import("@tinkoff/router").AbstractRouter>;
|
|
5
|
+
routeTransform: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-router").RouteTransform>;
|
|
6
|
+
routeResolve: {
|
|
7
|
+
token: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-router").RouteResolve>;
|
|
8
|
+
optional: boolean;
|
|
9
|
+
};
|
|
10
|
+
componentRegistry: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").ComponentRegistry>;
|
|
11
|
+
bundleManager: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").BundleManager>;
|
|
12
|
+
logger: import("@tinkoff/dippy").BaseTokenInterface<import("@tramvai/tokens-common").LoggerFactory>;
|
|
13
|
+
prefetchedLinksCache: import("@tinkoff/dippy").BaseTokenInterface<Set<string>>;
|
|
14
|
+
prefetchedLinksQueue: import("@tinkoff/dippy").BaseTokenInterface<{
|
|
15
|
+
add(run: () => Promise<void>): Promise<void>;
|
|
16
|
+
}>;
|
|
17
|
+
}, {
|
|
18
|
+
prefetch: (url: string) => Promise<void>;
|
|
19
|
+
}>)[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN } from '@tramvai/tokens-common';
|
|
2
2
|
import type { Router } from '@tinkoff/router';
|
|
3
|
-
|
|
3
|
+
type RouterOptions = Pick<ConstructorParameters<typeof Router>[0], 'onRedirect' | 'onNotFound' | 'onBlock' | 'defaultRedirectCode'>;
|
|
4
4
|
export declare const routerOptions: ({ requestManager, responseManager, }: {
|
|
5
5
|
requestManager: typeof REQUEST_MANAGER_TOKEN;
|
|
6
6
|
responseManager: typeof RESPONSE_MANAGER_TOKEN;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Provider } from '@tinkoff/dippy';
|
|
2
2
|
import type { Route } from '@tinkoff/router';
|
|
3
3
|
import { ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN } from '@tramvai/tokens-router';
|
|
4
|
-
|
|
4
|
+
type ForRoot<T = any> = (routes: Route[], options?: {
|
|
5
5
|
spaActionsMode?: typeof ROUTER_SPA_ACTIONS_RUN_MODE_TOKEN;
|
|
6
6
|
}) => {
|
|
7
7
|
mainModule: T;
|
package/lib/services/page.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Url } from '@tinkoff/url';
|
|
2
2
|
import type { NavigationRoute, NavigateOptions, UpdateCurrentRouteOptions, HistoryOptions } from '@tinkoff/router';
|
|
3
3
|
import type { TramvaiComponent } from '@tramvai/react';
|
|
4
|
-
import type { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
|
|
5
|
-
|
|
4
|
+
import type { PageServiceComponentType, PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
|
|
5
|
+
type PageServiceInterface = typeof PAGE_SERVICE_TOKEN;
|
|
6
6
|
export declare class PageService implements PageServiceInterface {
|
|
7
7
|
private router;
|
|
8
8
|
private componentRegistry;
|
|
@@ -22,7 +22,7 @@ export declare class PageService implements PageServiceInterface {
|
|
|
22
22
|
go(to: number, options?: HistoryOptions): Promise<void>;
|
|
23
23
|
addComponent(name: string, component: TramvaiComponent): void;
|
|
24
24
|
getComponent(name: string): TramvaiComponent | undefined;
|
|
25
|
-
resolveComponentFromConfig(property:
|
|
25
|
+
resolveComponentFromConfig(property: PageServiceComponentType): TramvaiComponent;
|
|
26
26
|
private getComponentsGroupName;
|
|
27
27
|
}
|
|
28
28
|
export {};
|
package/lib/test/unit/guard.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AbstractRouter } from '@tinkoff/router';
|
|
2
2
|
import { getDiWrapper } from '@tramvai/test-helpers';
|
|
3
|
-
|
|
3
|
+
type Options = Parameters<typeof getDiWrapper>[0] & {
|
|
4
4
|
router?: AbstractRouter;
|
|
5
5
|
};
|
|
6
6
|
export declare const testGuard: (options: Options) => {
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const requestIdleCallback: (((callback: IdleRequestCallback, options?: IdleRequestOptions) => number) & typeof globalThis.requestIdleCallback) | ((callback: () => void) => number);
|
|
2
|
+
export declare const cancelIdleCallback: ((handle: number) => void) & typeof globalThis.cancelIdleCallback;
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tramvai/module-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.44.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"browser": {
|
|
7
7
|
"./lib/modules/server.js": "./lib/modules/browser.js",
|
|
8
|
+
"./lib/hooks/usePrefetch.js": "./lib/hooks/usePrefetch.browser.js",
|
|
8
9
|
"./lib/index.es.js": "./lib/index.browser.js"
|
|
9
10
|
},
|
|
10
11
|
"typings": "lib/index.d.ts",
|
|
11
12
|
"files": [
|
|
12
13
|
"lib",
|
|
13
14
|
"tests.js",
|
|
14
|
-
"tests.d.ts"
|
|
15
|
+
"tests.d.ts",
|
|
16
|
+
"__migrations__"
|
|
15
17
|
],
|
|
16
18
|
"sideEffects": false,
|
|
17
19
|
"repository": {
|
|
@@ -25,25 +27,26 @@
|
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"@tinkoff/errors": "0.3.5",
|
|
28
|
-
"@tinkoff/router": "0.2.
|
|
30
|
+
"@tinkoff/router": "0.2.5",
|
|
29
31
|
"@tinkoff/url": "0.8.4",
|
|
30
|
-
"@tramvai/
|
|
31
|
-
"@tramvai/tokens-
|
|
32
|
-
"@tramvai/tokens-
|
|
33
|
-
"@tramvai/tokens-
|
|
34
|
-
"@tramvai/
|
|
32
|
+
"@tramvai/react": "2.44.2",
|
|
33
|
+
"@tramvai/tokens-child-app": "2.44.2",
|
|
34
|
+
"@tramvai/tokens-render": "2.44.2",
|
|
35
|
+
"@tramvai/tokens-router": "2.44.2",
|
|
36
|
+
"@tramvai/tokens-server": "2.44.2",
|
|
37
|
+
"@tramvai/experiments": "2.44.2"
|
|
35
38
|
},
|
|
36
39
|
"peerDependencies": {
|
|
37
40
|
"@tinkoff/utils": "^2.1.2",
|
|
38
|
-
"@tramvai/cli": "2.
|
|
39
|
-
"@tramvai/core": "2.
|
|
40
|
-
"@tramvai/module-log": "2.
|
|
41
|
-
"@tramvai/module-server": "2.
|
|
42
|
-
"@tramvai/papi": "2.
|
|
43
|
-
"@tramvai/state": "2.
|
|
44
|
-
"@tramvai/test-helpers": "2.
|
|
45
|
-
"@tramvai/test-mocks": "2.
|
|
46
|
-
"@tramvai/tokens-common": "2.
|
|
41
|
+
"@tramvai/cli": "2.44.2",
|
|
42
|
+
"@tramvai/core": "2.44.2",
|
|
43
|
+
"@tramvai/module-log": "2.44.2",
|
|
44
|
+
"@tramvai/module-server": "2.44.2",
|
|
45
|
+
"@tramvai/papi": "2.44.2",
|
|
46
|
+
"@tramvai/state": "2.44.2",
|
|
47
|
+
"@tramvai/test-helpers": "2.44.2",
|
|
48
|
+
"@tramvai/test-mocks": "2.44.2",
|
|
49
|
+
"@tramvai/tokens-common": "2.44.2",
|
|
47
50
|
"@tinkoff/dippy": "0.8.9",
|
|
48
51
|
"react": "*",
|
|
49
52
|
"tslib": "^2.4.0"
|