@alepha/react 0.9.2 → 0.9.4

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 (40) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +378 -325
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +570 -458
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +305 -213
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +304 -212
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +567 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +16 -13
  13. package/src/components/ErrorViewer.tsx +1 -1
  14. package/src/components/Link.tsx +4 -24
  15. package/src/components/NestedView.tsx +20 -9
  16. package/src/components/NotFound.tsx +5 -2
  17. package/src/descriptors/$page.ts +86 -12
  18. package/src/errors/Redirection.ts +13 -0
  19. package/src/hooks/useActive.ts +28 -30
  20. package/src/hooks/useAlepha.ts +16 -2
  21. package/src/hooks/useClient.ts +7 -2
  22. package/src/hooks/useInject.ts +4 -1
  23. package/src/hooks/useQueryParams.ts +9 -6
  24. package/src/hooks/useRouter.ts +18 -30
  25. package/src/hooks/useRouterEvents.ts +7 -4
  26. package/src/hooks/useRouterState.ts +8 -20
  27. package/src/hooks/useSchema.ts +10 -15
  28. package/src/hooks/useStore.ts +9 -8
  29. package/src/index.browser.ts +11 -11
  30. package/src/index.shared.ts +4 -5
  31. package/src/index.ts +21 -30
  32. package/src/providers/ReactBrowserProvider.ts +155 -65
  33. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  34. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
  35. package/src/providers/ReactServerProvider.ts +100 -68
  36. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
  37. package/src/contexts/RouterContext.ts +0 -14
  38. package/src/errors/RedirectionError.ts +0 -10
  39. package/src/providers/BrowserRouterProvider.ts +0 -146
  40. package/src/providers/ReactBrowserRenderer.ts +0 -93
@@ -2,17 +2,17 @@ import { $module } from "@alepha/core";
2
2
  import { AlephaServer } from "@alepha/server";
3
3
  import { AlephaServerLinks } from "@alepha/server-links";
4
4
  import { $page } from "./descriptors/$page.ts";
5
- import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
6
- import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
7
5
  import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
8
- import { ReactBrowserRenderer } from "./providers/ReactBrowserRenderer.ts";
6
+ import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
7
+ import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
8
+ import { ReactRouter } from "./services/ReactRouter.ts";
9
9
 
10
10
  // ---------------------------------------------------------------------------------------------------------------------
11
11
 
12
12
  export * from "./index.shared.ts";
13
- export * from "./providers/BrowserRouterProvider.ts";
14
- export * from "./providers/PageDescriptorProvider.ts";
15
13
  export * from "./providers/ReactBrowserProvider.ts";
14
+ export * from "./providers/ReactBrowserRouterProvider.ts";
15
+ export * from "./providers/ReactPageProvider.ts";
16
16
 
17
17
  // ---------------------------------------------------------------------------------------------------------------------
18
18
 
@@ -20,17 +20,17 @@ export const AlephaReact = $module({
20
20
  name: "alepha.react",
21
21
  descriptors: [$page],
22
22
  services: [
23
- PageDescriptorProvider,
24
- ReactBrowserRenderer,
25
- BrowserRouterProvider,
23
+ ReactPageProvider,
24
+ ReactBrowserRouterProvider,
26
25
  ReactBrowserProvider,
26
+ ReactRouter,
27
27
  ],
28
28
  register: (alepha) =>
29
29
  alepha
30
30
  .with(AlephaServer)
31
31
  .with(AlephaServerLinks)
32
- .with(PageDescriptorProvider)
32
+ .with(ReactPageProvider)
33
33
  .with(ReactBrowserProvider)
34
- .with(BrowserRouterProvider)
35
- .with(ReactBrowserRenderer),
34
+ .with(ReactBrowserRouterProvider)
35
+ .with(ReactRouter),
36
36
  });
@@ -1,15 +1,13 @@
1
1
  export { default as ClientOnly } from "./components/ClientOnly.tsx";
2
2
  export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
3
- export * from "./components/ErrorViewer.tsx";
4
- export { default as Link } from "./components/Link.tsx";
3
+ export { default as ErrorViewer } from "./components/ErrorViewer.tsx";
4
+ export { default as Link, type LinkProps } from "./components/Link.tsx";
5
5
  export { default as NestedView } from "./components/NestedView.tsx";
6
6
  export { default as NotFound } from "./components/NotFound.tsx";
7
7
  export * from "./contexts/AlephaContext.ts";
8
- export * from "./contexts/RouterContext.ts";
9
8
  export * from "./contexts/RouterLayerContext.ts";
10
9
  export * from "./descriptors/$page.ts";
11
- export * from "./errors/RedirectionError.ts";
12
- export * from "./hooks/RouterHookApi.ts";
10
+ export * from "./errors/Redirection.ts";
13
11
  export * from "./hooks/useActive.ts";
14
12
  export * from "./hooks/useAlepha.ts";
15
13
  export * from "./hooks/useClient.ts";
@@ -20,3 +18,4 @@ export * from "./hooks/useRouterEvents.ts";
20
18
  export * from "./hooks/useRouterState.ts";
21
19
  export * from "./hooks/useSchema.ts";
22
20
  export * from "./hooks/useStore.ts";
21
+ export * from "./services/ReactRouter.ts";
package/src/index.ts CHANGED
@@ -3,65 +3,55 @@ import { AlephaServer, type ServerRequest } from "@alepha/server";
3
3
  import { AlephaServerCache } from "@alepha/server-cache";
4
4
  import { AlephaServerLinks } from "@alepha/server-links";
5
5
  import { $page } from "./descriptors/$page.ts";
6
+ import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
6
7
  import {
7
- PageDescriptorProvider,
8
- type PageReactContext,
9
- type PageRequest,
10
- type RouterState,
11
- } from "./providers/PageDescriptorProvider.ts";
12
- import {
13
- ReactBrowserProvider,
14
- type ReactHydrationState,
15
- } from "./providers/ReactBrowserProvider.ts";
8
+ ReactPageProvider,
9
+ type ReactRouterState,
10
+ } from "./providers/ReactPageProvider.ts";
16
11
  import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
12
+ import { ReactRouter } from "./services/ReactRouter.ts";
17
13
 
18
14
  // ---------------------------------------------------------------------------------------------------------------------
19
15
 
20
16
  export * from "./index.shared.ts";
21
- export * from "./providers/PageDescriptorProvider.ts";
22
17
  export * from "./providers/ReactBrowserProvider.ts";
18
+ export * from "./providers/ReactPageProvider.ts";
23
19
  export * from "./providers/ReactServerProvider.ts";
24
20
 
25
21
  // ---------------------------------------------------------------------------------------------------------------------
26
22
 
27
23
  declare module "@alepha/core" {
24
+ interface State {
25
+ "react.router.state"?: ReactRouterState;
26
+ }
27
+
28
28
  interface Hooks {
29
- "react:router:createLayers": {
30
- request: ServerRequest;
31
- context: PageRequest;
32
- layers: PageRequest[];
33
- };
34
29
  "react:server:render:begin": {
35
30
  request?: ServerRequest;
36
- context: PageRequest;
31
+ state: ReactRouterState;
37
32
  };
38
33
  "react:server:render:end": {
39
34
  request?: ServerRequest;
40
- context: PageRequest;
41
- state: RouterState;
35
+ state: ReactRouterState;
42
36
  html: string;
43
37
  };
38
+ // -----------------------------------------------------------------------------------------------------------------
44
39
  "react:browser:render": {
45
- state: RouterState;
46
- context: PageReactContext;
40
+ state: ReactRouterState;
47
41
  hydration?: ReactHydrationState;
48
42
  };
49
43
  "react:transition:begin": {
50
- state: RouterState;
51
- context: PageReactContext;
44
+ state: ReactRouterState;
52
45
  };
53
46
  "react:transition:success": {
54
- state: RouterState;
55
- context: PageReactContext;
47
+ state: ReactRouterState;
56
48
  };
57
49
  "react:transition:error": {
50
+ state: ReactRouterState;
58
51
  error: Error;
59
- state: RouterState;
60
- context: PageReactContext;
61
52
  };
62
53
  "react:transition:end": {
63
- state: RouterState;
64
- context: PageReactContext;
54
+ state: ReactRouterState;
65
55
  };
66
56
  }
67
57
  }
@@ -81,12 +71,13 @@ declare module "@alepha/core" {
81
71
  export const AlephaReact = $module({
82
72
  name: "alepha.react",
83
73
  descriptors: [$page],
84
- services: [ReactServerProvider, PageDescriptorProvider, ReactBrowserProvider],
74
+ services: [ReactServerProvider, ReactPageProvider, ReactRouter],
85
75
  register: (alepha) =>
86
76
  alepha
87
77
  .with(AlephaServer)
88
78
  .with(AlephaServerCache)
89
79
  .with(AlephaServerLinks)
90
80
  .with(ReactServerProvider)
91
- .with(PageDescriptorProvider),
81
+ .with(ReactPageProvider)
82
+ .with(ReactRouter),
92
83
  });
@@ -1,74 +1,124 @@
1
- import { $hook, $inject, $logger, Alepha } from "@alepha/core";
2
- import type { ApiLinksResponse } from "@alepha/server";
1
+ import {
2
+ $env,
3
+ $hook,
4
+ $inject,
5
+ Alepha,
6
+ type State,
7
+ type Static,
8
+ t,
9
+ } from "@alepha/core";
10
+ import { DateTimeProvider } from "@alepha/datetime";
11
+ import { $logger } from "@alepha/logger";
3
12
  import { LinkProvider } from "@alepha/server-links";
4
- import type { Root } from "react-dom/client";
5
- import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
13
+ import { createRoot, hydrateRoot, type Root } from "react-dom/client";
14
+ import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
6
15
  import type {
7
16
  PreviousLayerData,
8
- RouterRenderResult,
9
- RouterState,
17
+ ReactRouterState,
10
18
  TransitionOptions,
11
- } from "./PageDescriptorProvider.ts";
19
+ } from "./ReactPageProvider.ts";
20
+
21
+ const envSchema = t.object({
22
+ REACT_ROOT_ID: t.string({ default: "root" }),
23
+ });
24
+
25
+ declare module "@alepha/core" {
26
+ interface Env extends Partial<Static<typeof envSchema>> {}
27
+ }
28
+
29
+ export interface ReactBrowserRendererOptions {
30
+ scrollRestoration?: "top" | "manual";
31
+ }
12
32
 
13
33
  export class ReactBrowserProvider {
34
+ protected readonly env = $env(envSchema);
14
35
  protected readonly log = $logger();
15
36
  protected readonly client = $inject(LinkProvider);
16
37
  protected readonly alepha = $inject(Alepha);
17
- protected readonly router = $inject(BrowserRouterProvider);
18
- protected root!: Root;
38
+ protected readonly router = $inject(ReactBrowserRouterProvider);
39
+ protected readonly dateTimeProvider = $inject(DateTimeProvider);
40
+ protected root?: Root;
41
+
42
+ public options: ReactBrowserRendererOptions = {
43
+ scrollRestoration: "top",
44
+ };
45
+
46
+ protected getRootElement() {
47
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
48
+ if (root) {
49
+ return root;
50
+ }
51
+
52
+ const div = this.document.createElement("div");
53
+ div.id = this.env.REACT_ROOT_ID;
54
+
55
+ this.document.body.prepend(div);
56
+
57
+ return div;
58
+ }
19
59
 
20
60
  public transitioning?: {
21
61
  to: string;
62
+ from?: string;
22
63
  };
23
64
 
24
- public state: RouterState = {
25
- layers: [],
26
- pathname: "",
27
- search: "",
28
- };
65
+ public get state(): ReactRouterState {
66
+ return this.alepha.state("react.router.state")!;
67
+ }
29
68
 
69
+ /**
70
+ * Accessor for Document DOM API.
71
+ */
30
72
  public get document() {
31
73
  return window.document;
32
74
  }
33
75
 
76
+ /**
77
+ * Accessor for History DOM API.
78
+ */
34
79
  public get history() {
35
80
  return window.history;
36
81
  }
37
82
 
83
+ /**
84
+ * Accessor for Location DOM API.
85
+ */
38
86
  public get location() {
39
87
  return window.location;
40
88
  }
41
89
 
42
- public get url(): string {
43
- let url = this.location.pathname + this.location.search;
44
-
45
- if (import.meta?.env?.BASE_URL) {
46
- url = url.replace(import.meta.env?.BASE_URL, "");
47
- if (!url.startsWith("/")) {
48
- url = `/${url}`;
49
- }
90
+ public get base() {
91
+ const base = import.meta.env?.BASE_URL;
92
+ if (!base || base === "/") {
93
+ return "";
50
94
  }
51
95
 
52
- return url;
96
+ return base;
53
97
  }
54
98
 
55
- public pushState(url: string, replace?: boolean) {
56
- let path = url;
57
-
58
- if (import.meta?.env?.BASE_URL) {
59
- path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
99
+ public get url(): string {
100
+ const url = this.location.pathname + this.location.search;
101
+ if (this.base) {
102
+ return url.replace(this.base, "");
60
103
  }
104
+ return url;
105
+ }
106
+
107
+ public pushState(path: string, replace?: boolean) {
108
+ const url = this.base + path;
61
109
 
62
110
  if (replace) {
63
- this.history.replaceState({}, "", path);
111
+ this.history.replaceState({}, "", url);
64
112
  } else {
65
- this.history.pushState({}, "", path);
113
+ this.history.pushState({}, "", url);
66
114
  }
67
115
  }
68
116
 
69
117
  public async invalidate(props?: Record<string, any>) {
70
118
  const previous: PreviousLayerData[] = [];
71
119
 
120
+ this.log.trace("Invalidating layers");
121
+
72
122
  if (props) {
73
123
  const [key] = Object.keys(props);
74
124
  const value = props[key];
@@ -92,48 +142,57 @@ export class ReactBrowserProvider {
92
142
  }
93
143
 
94
144
  public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
95
- const result = await this.render({
145
+ this.log.trace(`Going to ${url}`, {
96
146
  url,
147
+ options,
97
148
  });
98
149
 
99
- // when redirecting in browser
100
- if (result.context.url.pathname !== url) {
101
- // TODO: check if losing search params is acceptable?
102
- this.pushState(result.context.url.pathname);
103
- return;
104
- }
150
+ await this.render({
151
+ url,
152
+ previous: options.force ? [] : this.state.layers,
153
+ });
105
154
 
106
- if (options.replace) {
107
- this.pushState(url);
155
+ // when redirecting in browser
156
+ if (this.state.url.pathname + this.state.url.search !== url) {
157
+ this.pushState(this.state.url.pathname + this.state.url.search);
108
158
  return;
109
159
  }
110
160
 
111
- this.pushState(url);
161
+ this.pushState(url, options.replace);
112
162
  }
113
163
 
114
164
  protected async render(
115
165
  options: { url?: string; previous?: PreviousLayerData[] } = {},
116
- ): Promise<RouterRenderResult> {
166
+ ): Promise<void> {
117
167
  const previous = options.previous ?? this.state.layers;
118
168
  const url = options.url ?? this.url;
169
+ const start = this.dateTimeProvider.now();
119
170
 
120
- this.transitioning = { to: url };
171
+ this.transitioning = {
172
+ to: url,
173
+ from: this.state?.url.pathname,
174
+ };
121
175
 
122
- const result = await this.router.transition(
176
+ this.log.debug("Transitioning...", {
177
+ to: url,
178
+ });
179
+
180
+ const redirect = await this.router.transition(
123
181
  new URL(`http://localhost${url}`),
124
- {
125
- previous,
126
- state: this.state,
127
- },
182
+ previous,
128
183
  );
129
184
 
130
- if (result.redirect) {
131
- return await this.render({ url: result.redirect });
185
+ if (redirect) {
186
+ this.log.info("Redirecting to", {
187
+ redirect,
188
+ });
189
+ return await this.render({ url: redirect });
132
190
  }
133
191
 
134
- this.transitioning = undefined;
192
+ const ms = this.dateTimeProvider.now().diff(start);
193
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
135
194
 
136
- return result;
195
+ this.transitioning = undefined;
137
196
  }
138
197
 
139
198
  /**
@@ -151,33 +210,57 @@ export class ReactBrowserProvider {
151
210
 
152
211
  // -------------------------------------------------------------------------------------------------------------------
153
212
 
213
+ protected readonly onTransitionEnd = $hook({
214
+ on: "react:transition:end",
215
+ handler: () => {
216
+ if (
217
+ this.options.scrollRestoration === "top" &&
218
+ typeof window !== "undefined"
219
+ ) {
220
+ this.log.trace("Restoring scroll position to top");
221
+ window.scrollTo(0, 0);
222
+ }
223
+ },
224
+ });
225
+
154
226
  public readonly ready = $hook({
155
227
  on: "ready",
156
228
  handler: async () => {
157
229
  const hydration = this.getHydrationState();
158
230
  const previous = hydration?.layers ?? [];
159
231
 
160
- if (hydration?.links) {
161
- for (const link of hydration.links.links) {
162
- this.client.pushLink(link);
232
+ if (hydration) {
233
+ // low budget, but works for now
234
+ for (const [key, value] of Object.entries(hydration)) {
235
+ if (key !== "layers") {
236
+ this.alepha.state(key as keyof State, value);
237
+ }
163
238
  }
164
239
  }
165
240
 
166
- const { context } = await this.render({ previous });
241
+ await this.render({ previous });
167
242
 
168
- await this.alepha.emit("react:browser:render", {
169
- state: this.state,
170
- context,
171
- hydration,
172
- });
243
+ const element = this.router.root(this.state);
244
+ if (hydration?.layers) {
245
+ this.root = hydrateRoot(this.getRootElement(), element);
246
+ this.log.info("Hydrated root element");
247
+ } else {
248
+ this.root ??= createRoot(this.getRootElement());
249
+ this.root.render(element);
250
+ this.log.info("Created root element");
251
+ }
173
252
 
174
253
  window.addEventListener("popstate", () => {
175
- // when you update silently queryparams or hash, skip rendering
254
+ // when you update silently queryParams or hash, skip rendering
176
255
  // if you want to force a rendering, use #go()
177
- if (this.state.pathname === this.url) {
256
+ if (this.base + this.state.url.pathname === this.location.pathname) {
178
257
  return;
179
258
  }
180
259
 
260
+ this.log.debug("Popstate event triggered - rendering new state", {
261
+ url: this.location.pathname + this.location.search,
262
+ });
263
+
181
264
  this.render();
182
265
  });
183
266
  },
@@ -190,9 +273,16 @@ export interface RouterGoOptions {
190
273
  replace?: boolean;
191
274
  match?: TransitionOptions;
192
275
  params?: Record<string, string>;
276
+ query?: Record<string, string>;
277
+
278
+ /**
279
+ * Recreate the whole page, ignoring the current state.
280
+ */
281
+ force?: boolean;
193
282
  }
194
283
 
195
- export interface ReactHydrationState {
284
+ export type ReactHydrationState = {
196
285
  layers?: Array<PreviousLayerData>;
197
- links?: ApiLinksResponse;
198
- }
286
+ } & {
287
+ [key: string]: any;
288
+ };
@@ -0,0 +1,132 @@
1
+ import { $hook, $inject, Alepha } from "@alepha/core";
2
+ import { $logger } from "@alepha/logger";
3
+ import { type Route, RouterProvider } from "@alepha/router";
4
+ import { createElement, type ReactNode } from "react";
5
+ import NotFoundPage from "../components/NotFound.tsx";
6
+ import {
7
+ isPageRoute,
8
+ type PageRoute,
9
+ type PageRouteEntry,
10
+ type PreviousLayerData,
11
+ ReactPageProvider,
12
+ type ReactRouterState,
13
+ } from "./ReactPageProvider.ts";
14
+
15
+ export interface BrowserRoute extends Route {
16
+ page: PageRoute;
17
+ }
18
+
19
+ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
20
+ protected readonly log = $logger();
21
+ protected readonly alepha = $inject(Alepha);
22
+ protected readonly pageApi = $inject(ReactPageProvider);
23
+
24
+ public add(entry: PageRouteEntry) {
25
+ this.pageApi.add(entry);
26
+ }
27
+
28
+ protected readonly configure = $hook({
29
+ on: "configure",
30
+ handler: async () => {
31
+ for (const page of this.pageApi.getPages()) {
32
+ // mount only if a view is provided
33
+ if (page.component || page.lazy) {
34
+ this.push({
35
+ path: page.match,
36
+ page,
37
+ });
38
+ }
39
+ }
40
+ },
41
+ });
42
+
43
+ public async transition(
44
+ url: URL,
45
+ previous: PreviousLayerData[] = [],
46
+ ): Promise<string | void> {
47
+ const { pathname, search } = url;
48
+
49
+ const entry: Partial<ReactRouterState> = {
50
+ url,
51
+ query: {},
52
+ params: {},
53
+ layers: [],
54
+ onError: () => null,
55
+ };
56
+
57
+ const state = entry as ReactRouterState;
58
+
59
+ await this.alepha.emit("react:transition:begin", { state });
60
+
61
+ try {
62
+ const { route, params } = this.match(pathname);
63
+
64
+ const query: Record<string, string> = {};
65
+ if (search) {
66
+ for (const [key, value] of new URLSearchParams(search).entries()) {
67
+ query[key] = String(value);
68
+ }
69
+ }
70
+
71
+ state.query = query;
72
+ state.params = params ?? {};
73
+
74
+ if (isPageRoute(route)) {
75
+ const { redirect } = await this.pageApi.createLayers(
76
+ route.page,
77
+ state,
78
+ previous,
79
+ );
80
+ if (redirect) {
81
+ return redirect;
82
+ }
83
+ }
84
+
85
+ if (state.layers.length === 0) {
86
+ state.layers.push({
87
+ name: "not-found",
88
+ element: createElement(NotFoundPage),
89
+ index: 0,
90
+ path: "/",
91
+ });
92
+ }
93
+
94
+ await this.alepha.emit("react:transition:success", { state });
95
+ } catch (e) {
96
+ this.log.error("Transition has failed", e);
97
+ state.layers = [
98
+ {
99
+ name: "error",
100
+ element: this.pageApi.renderError(e as Error),
101
+ index: 0,
102
+ path: "/",
103
+ },
104
+ ];
105
+
106
+ await this.alepha.emit("react:transition:error", {
107
+ error: e as Error,
108
+ state,
109
+ });
110
+ }
111
+
112
+ // [feature]: local hook for leaving a page
113
+ if (previous) {
114
+ for (let i = 0; i < previous.length; i++) {
115
+ const layer = previous[i];
116
+ if (state.layers[i]?.name !== layer.name) {
117
+ this.pageApi.page(layer.name)?.onLeave?.();
118
+ }
119
+ }
120
+ }
121
+
122
+ await this.alepha.emit("react:transition:end", {
123
+ state,
124
+ });
125
+
126
+ this.alepha.state("react.router.state", state);
127
+ }
128
+
129
+ public root(state: ReactRouterState): ReactNode {
130
+ return this.pageApi.root(state);
131
+ }
132
+ }