@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.12 → 3.2.0-ultramodern.120

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.
Files changed (87) hide show
  1. package/dist/cjs/cli/index.js +47 -9
  2. package/dist/cjs/cli/routeSplitting.js +87 -0
  3. package/dist/cjs/cli/tanstackTypes.js +230 -63
  4. package/dist/cjs/cli.js +12 -8
  5. package/dist/cjs/runtime/DefaultNotFound.js +9 -5
  6. package/dist/cjs/runtime/basepathRewrite.js +12 -8
  7. package/dist/cjs/runtime/dataMutation.js +9 -5
  8. package/dist/cjs/runtime/hooks.js +9 -5
  9. package/dist/cjs/runtime/hydrationBoundary.js +48 -0
  10. package/dist/cjs/runtime/index.js +330 -74
  11. package/dist/cjs/runtime/lifecycle.js +15 -11
  12. package/dist/cjs/runtime/outlet.js +58 -0
  13. package/dist/cjs/runtime/plugin.js +203 -98
  14. package/dist/cjs/runtime/plugin.node.js +38 -16
  15. package/dist/cjs/runtime/plugin.worker.js +53 -0
  16. package/dist/cjs/runtime/prefetchLink.js +10 -6
  17. package/dist/cjs/runtime/routeTree.js +81 -17
  18. package/dist/cjs/runtime/rsc/ClientSlot.js +9 -5
  19. package/dist/cjs/runtime/rsc/CompositeComponent.js +9 -5
  20. package/dist/cjs/runtime/rsc/ReplayableStream.js +14 -9
  21. package/dist/cjs/runtime/rsc/RscNodeRenderer.js +9 -5
  22. package/dist/cjs/runtime/rsc/SlotContext.js +9 -5
  23. package/dist/cjs/runtime/rsc/client.js +9 -5
  24. package/dist/cjs/runtime/rsc/createRscProxy.js +9 -5
  25. package/dist/cjs/runtime/rsc/index.js +9 -5
  26. package/dist/cjs/runtime/rsc/payloadRouter.js +9 -5
  27. package/dist/cjs/runtime/rsc/server.js +9 -5
  28. package/dist/cjs/runtime/rsc/slotUsageSanitizer.js +9 -5
  29. package/dist/cjs/runtime/rsc/symbols.js +20 -15
  30. package/dist/cjs/runtime/types.js +31 -1
  31. package/dist/cjs/runtime/utils.js +9 -5
  32. package/dist/cjs/runtime.js +9 -5
  33. package/dist/esm/cli/index.mjs +28 -6
  34. package/dist/esm/cli/routeSplitting.mjs +43 -0
  35. package/dist/esm/cli/tanstackTypes.mjs +219 -59
  36. package/dist/esm/runtime/hydrationBoundary.mjs +10 -0
  37. package/dist/esm/runtime/index.mjs +3 -2
  38. package/dist/esm/runtime/outlet.mjs +17 -0
  39. package/dist/esm/runtime/plugin.mjs +197 -96
  40. package/dist/esm/runtime/plugin.node.mjs +30 -12
  41. package/dist/esm/runtime/plugin.worker.mjs +1 -0
  42. package/dist/esm/runtime/prefetchLink.mjs +1 -1
  43. package/dist/esm/runtime/routeTree.mjs +73 -13
  44. package/dist/esm/runtime/types.mjs +7 -0
  45. package/dist/esm-node/cli/index.mjs +28 -6
  46. package/dist/esm-node/cli/routeSplitting.mjs +44 -0
  47. package/dist/esm-node/cli/tanstackTypes.mjs +219 -59
  48. package/dist/esm-node/runtime/hydrationBoundary.mjs +11 -0
  49. package/dist/esm-node/runtime/index.mjs +3 -2
  50. package/dist/esm-node/runtime/outlet.mjs +18 -0
  51. package/dist/esm-node/runtime/plugin.mjs +197 -96
  52. package/dist/esm-node/runtime/plugin.node.mjs +30 -12
  53. package/dist/esm-node/runtime/plugin.worker.mjs +2 -0
  54. package/dist/esm-node/runtime/prefetchLink.mjs +1 -1
  55. package/dist/esm-node/runtime/routeTree.mjs +73 -13
  56. package/dist/esm-node/runtime/types.mjs +7 -0
  57. package/dist/types/cli/index.d.ts +7 -1
  58. package/dist/types/cli/routeSplitting.d.ts +29 -0
  59. package/dist/types/cli/tanstackTypes.d.ts +9 -0
  60. package/dist/types/runtime/hooks.d.ts +9 -24
  61. package/dist/types/runtime/hydrationBoundary.d.ts +2 -0
  62. package/dist/types/runtime/index.d.ts +5 -2
  63. package/dist/types/runtime/outlet.d.ts +2 -0
  64. package/dist/types/runtime/plugin.d.ts +1 -1
  65. package/dist/types/runtime/plugin.node.d.ts +1 -1
  66. package/dist/types/runtime/plugin.worker.d.ts +1 -0
  67. package/dist/types/runtime/types.d.ts +7 -0
  68. package/package.json +20 -20
  69. package/src/cli/index.ts +59 -2
  70. package/src/cli/routeSplitting.ts +81 -0
  71. package/src/cli/tanstackTypes.ts +347 -67
  72. package/src/runtime/hydrationBoundary.tsx +12 -0
  73. package/src/runtime/index.tsx +107 -2
  74. package/src/runtime/outlet.tsx +48 -0
  75. package/src/runtime/plugin.node.tsx +58 -8
  76. package/src/runtime/plugin.tsx +372 -157
  77. package/src/runtime/plugin.worker.tsx +4 -0
  78. package/src/runtime/prefetchLink.tsx +1 -1
  79. package/src/runtime/routeTree.ts +194 -23
  80. package/src/runtime/ssr-shim.d.ts +1 -3
  81. package/src/runtime/types.ts +13 -0
  82. package/tests/router/cli.test.ts +315 -0
  83. package/tests/router/fastDefaults.test.ts +25 -0
  84. package/tests/router/hydrationBoundary.test.tsx +23 -0
  85. package/tests/router/prefetchLink.test.tsx +43 -7
  86. package/tests/router/routeTree.test.ts +416 -1
  87. package/tests/router/tanstackTypes.test.ts +415 -1
@@ -1,10 +1,16 @@
1
1
  import type { RouteObject } from '@modern-js/runtime-utils/router';
2
2
  import type { NestedRoute } from '@modern-js/types';
3
3
  import { createMemoryHistory } from '@tanstack/history';
4
- import { createRouter } from '@tanstack/react-router';
4
+ import { createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
5
+ import type { ComponentType } from 'react';
6
+ import { createElement, lazy } from 'react';
7
+ import { renderToStaticMarkup, renderToString } from 'react-dom/server';
8
+ import { Outlet as PublicOutlet } from '../../src/runtime';
9
+ import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
5
10
  import {
6
11
  createRouteTreeFromModernRoutes,
7
12
  createRouteTreeFromRouteObjects,
13
+ getModernRouteIdsFromMatches,
8
14
  } from '../../src/runtime/routeTree';
9
15
  import { __setTanstackRscPayloadDecoderForTests } from '../../src/runtime/rsc/payloadRouter';
10
16
  import { createRouteObjectsFromConfig } from '../../src/runtime/utils';
@@ -23,6 +29,9 @@ type TestRouteObject = RouteObject & {
23
29
  hasLoader?: boolean;
24
30
  inValidSSRRoute?: boolean;
25
31
  isClientComponent?: boolean;
32
+ lazyImport?: () => Promise<unknown>;
33
+ loaderDeps?: unknown;
34
+ validateSearch?: unknown;
26
35
  };
27
36
 
28
37
  type TestNestedRoute = NestedRoute & {
@@ -30,6 +39,8 @@ type TestNestedRoute = NestedRoute & {
30
39
  hasAction?: boolean;
31
40
  hasClientLoader?: boolean;
32
41
  hasLoader?: boolean;
42
+ loaderDeps?: unknown;
43
+ validateSearch?: unknown;
33
44
  };
34
45
 
35
46
  type ShouldRevalidateArgs = {
@@ -48,12 +59,21 @@ type ShouldReloadArgs = {
48
59
 
49
60
  type TestRoute = {
50
61
  options: {
62
+ component?: unknown;
51
63
  shouldReload?: (args: ShouldReloadArgs) => boolean | undefined;
52
64
  ssr?: boolean;
53
65
  staticData: Record<string, unknown>;
66
+ loaderDeps?: unknown;
67
+ validateSearch?: unknown;
68
+ wrapInSuspense?: unknown;
54
69
  };
55
70
  };
56
71
 
72
+ type PreloadableTestComponent = {
73
+ load?: () => Promise<unknown>;
74
+ preload?: () => Promise<unknown>;
75
+ };
76
+
57
77
  type TestRouter = {
58
78
  load: () => Promise<void>;
59
79
  looseRoutesById: Partial<Record<string, TestRoute>>;
@@ -107,6 +127,16 @@ function getLooseRouteByModernRouteId(
107
127
  return route;
108
128
  }
109
129
 
130
+ function countCompletedSuspenseBoundaries(markup: string) {
131
+ return markup.match(/<!--\$-->/g)?.length || 0;
132
+ }
133
+
134
+ describe('tanstack runtime public exports', () => {
135
+ test('exports the Modern Outlet implementation from the runtime entrypoint', () => {
136
+ expect(PublicOutlet).toBe(ModernOutlet);
137
+ });
138
+ });
139
+
110
140
  describe('tanstack route tree from RouteObject[]', () => {
111
141
  afterEach(() => {
112
142
  __setTanstackRscPayloadDecoderForTests();
@@ -145,6 +175,151 @@ describe('tanstack route tree from RouteObject[]', () => {
145
175
  expect(userMatch?.loaderData).toEqual({ id: '123' });
146
176
  });
147
177
 
178
+ test('does not force Suspense wrappers for ordinary generated routes', async () => {
179
+ const routes: RouteObject[] = [
180
+ {
181
+ id: 'root',
182
+ path: '/',
183
+ Component: () => createElement(Outlet),
184
+ children: [
185
+ {
186
+ id: 'plain',
187
+ path: 'plain',
188
+ Component: () => null,
189
+ },
190
+ ],
191
+ },
192
+ ];
193
+
194
+ const routeTree = createRouteTreeFromRouteObjects(routes);
195
+ const router = await loadRouteTree(routeTree, '/plain');
196
+
197
+ expect(routeTree.options.wrapInSuspense).toBeUndefined();
198
+ expect(
199
+ getLooseRoute(router, '/plain').options.wrapInSuspense,
200
+ ).toBeUndefined();
201
+ });
202
+
203
+ test('renders Modern Outlet through TanStack native outlet', async () => {
204
+ const routes: RouteObject[] = [
205
+ {
206
+ id: 'root',
207
+ path: '/',
208
+ Component: () =>
209
+ createElement('section', null, createElement(ModernOutlet)),
210
+ children: [
211
+ {
212
+ id: 'plain',
213
+ path: 'plain',
214
+ Component: () => createElement('main', null, 'Plain child route'),
215
+ },
216
+ ],
217
+ },
218
+ ];
219
+
220
+ const routeTree = createRouteTreeFromRouteObjects(routes);
221
+ const router = await loadRouteTree(routeTree, '/plain');
222
+ const markup = renderToString(
223
+ createElement(RouterProvider, { router } as never),
224
+ );
225
+ const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
226
+
227
+ expect(markup).toContain('Plain child route');
228
+ expect(suspenseBoundaryCount).toBe(1);
229
+ });
230
+
231
+ test('resolves matched Modern route ids from TanStack route registry fallback', () => {
232
+ const router = {
233
+ state: {
234
+ matches: [
235
+ { routeId: '__root__' },
236
+ { routeId: '/$lang' },
237
+ { routeId: '/$lang/tractors' },
238
+ {
239
+ route: {
240
+ options: {
241
+ staticData: {
242
+ modernRouteId: '(lang)/stores/page',
243
+ },
244
+ },
245
+ },
246
+ routeId: '/$lang/stores',
247
+ },
248
+ ],
249
+ },
250
+ routesById: {
251
+ __root__: {
252
+ options: {
253
+ staticData: {
254
+ modernRouteId: 'layout',
255
+ },
256
+ },
257
+ },
258
+ '/$lang': {
259
+ options: {
260
+ staticData: {
261
+ modernRouteId: '(lang)/page',
262
+ },
263
+ },
264
+ },
265
+ '/$lang/tractors': {
266
+ options: {
267
+ staticData: {
268
+ modernRouteId: '(lang)/tractors/page',
269
+ },
270
+ },
271
+ },
272
+ },
273
+ };
274
+
275
+ expect(getModernRouteIdsFromMatches(router as never)).toEqual([
276
+ 'layout',
277
+ '(lang)/page',
278
+ '(lang)/tractors/page',
279
+ '(lang)/stores/page',
280
+ ]);
281
+ });
282
+
283
+ test('preserves TanStack search contracts from RouteObject routes', () => {
284
+ const rootValidateSearch = (search: unknown) => ({ root: search });
285
+ const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
286
+ const childValidateSearch = (search: unknown) => ({ child: search });
287
+ const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
288
+ const routes: TestRouteObject[] = [
289
+ {
290
+ id: 'root',
291
+ path: '/',
292
+ validateSearch: rootValidateSearch,
293
+ loaderDeps: rootLoaderDeps,
294
+ Component: () => null,
295
+ children: [
296
+ {
297
+ id: 'search',
298
+ path: 'search',
299
+ validateSearch: childValidateSearch,
300
+ loaderDeps: childLoaderDeps,
301
+ Component: () => null,
302
+ },
303
+ ],
304
+ },
305
+ ];
306
+
307
+ const routeTree = createRouteTreeFromRouteObjects(routes);
308
+ const router = createRouter({
309
+ routeTree,
310
+ history: createMemoryHistory({
311
+ initialEntries: ['/search'],
312
+ }),
313
+ context: {},
314
+ }) as unknown as TestRouter;
315
+ const searchRoute = getLooseRoute(router, '/search');
316
+
317
+ expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
318
+ expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
319
+ expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
320
+ expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
321
+ });
322
+
148
323
  test('uses TanStack route ids when loading RSC payload route data', async () => {
149
324
  const rootLoader = rstest.fn(() => ({ source: 'modern-root' }));
150
325
  const userLoader = rstest.fn(() => ({ source: 'modern-user' }));
@@ -234,6 +409,145 @@ describe('tanstack route tree from RouteObject[]', () => {
234
409
  expect(splatParamValue).toBe('a/b/c');
235
410
  });
236
411
 
412
+ test('preloads lazy Modern route components for server rendering', async () => {
413
+ const LazyRouteComponent = () =>
414
+ createElement('main', null, 'Lazy route ready');
415
+ const lazyImport = rstest.fn(async () => ({
416
+ default: LazyRouteComponent,
417
+ }));
418
+ const routes: TestRouteObject[] = [
419
+ {
420
+ id: 'root',
421
+ path: '/',
422
+ Component: () => null,
423
+ children: [
424
+ {
425
+ id: 'lazy',
426
+ path: 'lazy',
427
+ Component: lazy(lazyImport),
428
+ lazyImport,
429
+ },
430
+ ],
431
+ },
432
+ ];
433
+
434
+ const routeTree = createRouteTreeFromRouteObjects(routes);
435
+ const router = await loadRouteTree(routeTree, '/lazy');
436
+ const lazyRoute = getLooseRoute(router, '/lazy');
437
+ const component = lazyRoute.options.component as ComponentType & {
438
+ preload?: () => Promise<unknown>;
439
+ };
440
+
441
+ await component.preload?.();
442
+
443
+ expect(lazyImport).toHaveBeenCalled();
444
+ });
445
+
446
+ test('renders preloaded lazy child routes through TanStack router SSR', async () => {
447
+ const LazyRouteComponent = () =>
448
+ createElement('main', null, 'Lazy child route ready');
449
+ const lazyImport = rstest.fn(async () => ({
450
+ default: LazyRouteComponent,
451
+ }));
452
+ const routes: TestRouteObject[] = [
453
+ {
454
+ id: 'root',
455
+ path: '/',
456
+ Component: () => createElement('section', null, createElement(Outlet)),
457
+ children: [
458
+ {
459
+ id: 'lazy',
460
+ path: 'lazy',
461
+ Component: lazy(lazyImport),
462
+ lazyImport,
463
+ },
464
+ ],
465
+ },
466
+ ];
467
+
468
+ const routeTree = createRouteTreeFromRouteObjects(routes);
469
+ const router = await loadRouteTree(routeTree, '/lazy');
470
+ const lazyRoute = getLooseRoute(router, '/lazy');
471
+ const lazyComponent = lazyRoute.options
472
+ .component as PreloadableTestComponent;
473
+
474
+ expect(
475
+ renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
476
+ ).toContain('Lazy child route ready');
477
+ expect(typeof lazyComponent.load).toBe('function');
478
+ expect(typeof lazyComponent.preload).toBe('function');
479
+ });
480
+
481
+ test('exposes load-only Modern route components through TanStack preload', async () => {
482
+ const load = rstest.fn(async () => 'route chunk loaded');
483
+ const LoadOnlyRouteComponent = (() =>
484
+ createElement('main', null, 'Load-only route ready')) as ComponentType & {
485
+ load?: () => Promise<unknown>;
486
+ preload?: () => Promise<unknown>;
487
+ };
488
+ LoadOnlyRouteComponent.load = load;
489
+ const routes: TestRouteObject[] = [
490
+ {
491
+ id: 'root',
492
+ path: '/',
493
+ Component: () => createElement('section', null, createElement(Outlet)),
494
+ children: [
495
+ {
496
+ id: 'load-only',
497
+ path: 'load-only',
498
+ Component: LoadOnlyRouteComponent,
499
+ },
500
+ ],
501
+ },
502
+ ];
503
+
504
+ const routeTree = createRouteTreeFromRouteObjects(routes);
505
+ const router = await loadRouteTree(routeTree, '/load-only');
506
+ const loadOnlyRoute = getLooseRoute(router, '/load-only');
507
+ const loadOnlyComponent = loadOnlyRoute.options
508
+ .component as PreloadableTestComponent;
509
+
510
+ expect(typeof loadOnlyComponent.load).toBe('function');
511
+ expect(typeof loadOnlyComponent.preload).toBe('function');
512
+ expect(load).toHaveBeenCalledTimes(1);
513
+ await loadOnlyComponent.preload?.();
514
+ expect(load).toHaveBeenCalledTimes(2);
515
+ });
516
+
517
+ test('unwraps nested ESM route module defaults before server rendering', async () => {
518
+ const LazyRouteComponent = () =>
519
+ createElement('main', null, 'Nested lazy child route ready');
520
+ const lazyImport = rstest.fn(async () => ({
521
+ default: {
522
+ default: LazyRouteComponent,
523
+ },
524
+ }));
525
+ const routes: TestRouteObject[] = [
526
+ {
527
+ id: 'root',
528
+ path: '/',
529
+ Component: () => createElement('section', null, createElement(Outlet)),
530
+ children: [
531
+ {
532
+ id: 'lazy',
533
+ path: 'lazy',
534
+ Component: lazy(
535
+ lazyImport as () => Promise<{ default: ComponentType }>,
536
+ ),
537
+ lazyImport,
538
+ },
539
+ ],
540
+ },
541
+ ];
542
+
543
+ const routeTree = createRouteTreeFromRouteObjects(routes);
544
+ const router = await loadRouteTree(routeTree, '/lazy');
545
+
546
+ expect(
547
+ renderToStaticMarkup(createElement(RouterProvider, { router } as never)),
548
+ ).toContain('Nested lazy child route ready');
549
+ });
550
+
237
551
  test('preserves route handle and maps shouldRevalidate to shouldReload', async () => {
238
552
  const shouldRevalidate = rstest.fn(({ nextUrl }: ShouldRevalidateArgs) =>
239
553
  nextUrl.pathname.endsWith('/456'),
@@ -417,6 +731,107 @@ describe('tanstack route tree from RouteObject[]', () => {
417
731
  });
418
732
  });
419
733
 
734
+ test('preserves TanStack search contracts from Modern generated routes', () => {
735
+ const rootValidateSearch = (search: unknown) => ({ root: search });
736
+ const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
737
+ const childValidateSearch = (search: unknown) => ({ child: search });
738
+ const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
739
+ const modernRoutes: TestNestedRoute[] = [
740
+ {
741
+ type: 'nested',
742
+ origin: 'config',
743
+ id: 'root',
744
+ isRoot: true,
745
+ validateSearch: rootValidateSearch,
746
+ loaderDeps: rootLoaderDeps,
747
+ children: [
748
+ {
749
+ type: 'nested',
750
+ origin: 'config',
751
+ id: 'search',
752
+ path: 'search',
753
+ validateSearch: childValidateSearch,
754
+ loaderDeps: childLoaderDeps,
755
+ },
756
+ ],
757
+ },
758
+ ];
759
+ const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
760
+ const router = createRouter({
761
+ routeTree,
762
+ history: createMemoryHistory({
763
+ initialEntries: ['/search'],
764
+ }),
765
+ context: {},
766
+ }) as unknown as TestRouter;
767
+ const searchRoute = getLooseRouteByModernRouteId(router, 'search');
768
+
769
+ expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
770
+ expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
771
+ expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
772
+ expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
773
+ });
774
+
775
+ test('does not force Suspense wrappers for ordinary Modern routes', async () => {
776
+ const modernRoutes: TestNestedRoute[] = [
777
+ {
778
+ type: 'nested',
779
+ origin: 'config',
780
+ id: 'root',
781
+ isRoot: true,
782
+ component: () => createElement(Outlet),
783
+ children: [
784
+ {
785
+ type: 'nested',
786
+ origin: 'config',
787
+ id: 'plain',
788
+ path: 'plain',
789
+ component: () => null,
790
+ },
791
+ ],
792
+ },
793
+ ];
794
+
795
+ const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
796
+ const router = await loadRouteTree(routeTree, '/plain');
797
+ const plain = getLooseRouteByModernRouteId(router, 'plain');
798
+
799
+ expect(routeTree.options.wrapInSuspense).toBeUndefined();
800
+ expect(plain.options.wrapInSuspense).toBeUndefined();
801
+ });
802
+
803
+ test('renders Modern generated Outlet through TanStack native outlet', async () => {
804
+ const modernRoutes: TestNestedRoute[] = [
805
+ {
806
+ type: 'nested',
807
+ origin: 'config',
808
+ id: 'root',
809
+ isRoot: true,
810
+ component: () =>
811
+ createElement('section', null, createElement(ModernOutlet)),
812
+ children: [
813
+ {
814
+ type: 'nested',
815
+ origin: 'config',
816
+ id: 'plain',
817
+ path: 'plain',
818
+ component: () => createElement('main', null, 'Plain child route'),
819
+ },
820
+ ],
821
+ },
822
+ ];
823
+
824
+ const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
825
+ const router = await loadRouteTree(routeTree, '/plain');
826
+ const markup = renderToString(
827
+ createElement(RouterProvider, { router } as never),
828
+ );
829
+ const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
830
+
831
+ expect(markup).toContain('Plain child route');
832
+ expect(suspenseBoundaryCount).toBe(1);
833
+ });
834
+
420
835
  test('preserves Modern generated client route metadata', () => {
421
836
  const modernRoutes: TestNestedRoute[] = [
422
837
  {