@alepha/react 0.10.5 → 0.10.7
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/dist/index.browser.js +32 -48
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +39 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -63
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/components/ClientOnly.tsx +12 -12
- package/src/components/ErrorBoundary.tsx +43 -43
- package/src/components/ErrorViewer.tsx +140 -140
- package/src/components/Link.tsx +7 -7
- package/src/components/NestedView.tsx +177 -177
- package/src/components/NotFound.tsx +19 -19
- package/src/contexts/RouterLayerContext.ts +3 -3
- package/src/descriptors/$page.ts +292 -290
- package/src/errors/Redirection.ts +5 -5
- package/src/hooks/useActive.ts +41 -41
- package/src/hooks/useAlepha.ts +7 -7
- package/src/hooks/useClient.ts +5 -5
- package/src/hooks/useInject.ts +2 -2
- package/src/hooks/useQueryParams.ts +37 -37
- package/src/hooks/useRouter.ts +1 -1
- package/src/hooks/useRouterEvents.ts +46 -46
- package/src/hooks/useRouterState.ts +5 -5
- package/src/hooks/useSchema.ts +55 -55
- package/src/hooks/useStore.ts +25 -25
- package/src/index.browser.ts +18 -18
- package/src/index.ts +49 -49
- package/src/providers/ReactBrowserProvider.ts +268 -261
- package/src/providers/ReactBrowserRendererProvider.ts +15 -15
- package/src/providers/ReactBrowserRouterProvider.ts +124 -124
- package/src/providers/ReactPageProvider.ts +616 -618
- package/src/providers/ReactServerProvider.ts +505 -505
- package/src/services/ReactRouter.ts +191 -191
|
@@ -1,293 +1,300 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
Alepha,
|
|
6
|
+
type State,
|
|
7
|
+
type Static,
|
|
8
|
+
t,
|
|
9
9
|
} from "@alepha/core";
|
|
10
10
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
11
11
|
import { $logger } from "@alepha/logger";
|
|
12
12
|
import { LinkProvider } from "@alepha/server-links";
|
|
13
13
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
14
14
|
import type {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
PreviousLayerData,
|
|
16
|
+
ReactRouterState,
|
|
17
|
+
TransitionOptions,
|
|
18
18
|
} from "./ReactPageProvider.ts";
|
|
19
19
|
|
|
20
20
|
const envSchema = t.object({
|
|
21
|
-
|
|
21
|
+
REACT_ROOT_ID: t.text({ default: "root" }),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
declare module "@alepha/core" {
|
|
25
|
-
|
|
25
|
+
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface ReactBrowserRendererOptions {
|
|
29
|
-
|
|
29
|
+
scrollRestoration?: "top" | "manual";
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export class ReactBrowserProvider {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
33
|
+
protected readonly env = $env(envSchema);
|
|
34
|
+
protected readonly log = $logger();
|
|
35
|
+
protected readonly client = $inject(LinkProvider);
|
|
36
|
+
protected readonly alepha = $inject(Alepha);
|
|
37
|
+
protected readonly router = $inject(ReactBrowserRouterProvider);
|
|
38
|
+
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
39
|
+
|
|
40
|
+
public options: ReactBrowserRendererOptions = {
|
|
41
|
+
scrollRestoration: "top",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
protected getRootElement() {
|
|
45
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
46
|
+
if (root) {
|
|
47
|
+
return root;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const div = this.document.createElement("div");
|
|
51
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
52
|
+
|
|
53
|
+
this.document.body.prepend(div);
|
|
54
|
+
|
|
55
|
+
return div;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public transitioning?: {
|
|
59
|
+
to: string;
|
|
60
|
+
from?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
public get state(): ReactRouterState {
|
|
64
|
+
return this.alepha.state.get("react.router.state")!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Accessor for Document DOM API.
|
|
69
|
+
*/
|
|
70
|
+
public get document() {
|
|
71
|
+
return window.document;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Accessor for History DOM API.
|
|
76
|
+
*/
|
|
77
|
+
public get history() {
|
|
78
|
+
return window.history;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Accessor for Location DOM API.
|
|
83
|
+
*/
|
|
84
|
+
public get location() {
|
|
85
|
+
return window.location;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public get base() {
|
|
89
|
+
const base = import.meta.env?.BASE_URL;
|
|
90
|
+
if (!base || base === "/") {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return base;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public get url(): string {
|
|
98
|
+
const url = this.location.pathname + this.location.search;
|
|
99
|
+
if (this.base) {
|
|
100
|
+
return url.replace(this.base, "");
|
|
101
|
+
}
|
|
102
|
+
return url;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public pushState(path: string, replace?: boolean) {
|
|
106
|
+
const url = this.base + path;
|
|
107
|
+
|
|
108
|
+
if (replace) {
|
|
109
|
+
this.history.replaceState({}, "", url);
|
|
110
|
+
} else {
|
|
111
|
+
this.history.pushState({}, "", url);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async invalidate(props?: Record<string, any>) {
|
|
116
|
+
const previous: PreviousLayerData[] = [];
|
|
117
|
+
|
|
118
|
+
this.log.trace("Invalidating layers");
|
|
119
|
+
|
|
120
|
+
if (props) {
|
|
121
|
+
const [key] = Object.keys(props);
|
|
122
|
+
const value = props[key];
|
|
123
|
+
|
|
124
|
+
for (const layer of this.state.layers) {
|
|
125
|
+
if (layer.props?.[key]) {
|
|
126
|
+
previous.push({
|
|
127
|
+
...layer,
|
|
128
|
+
props: {
|
|
129
|
+
...layer.props,
|
|
130
|
+
[key]: value,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
previous.push(layer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await this.render({ previous });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
|
|
143
|
+
this.log.trace(`Going to ${url}`, {
|
|
144
|
+
url,
|
|
145
|
+
options,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await this.render({
|
|
149
|
+
url,
|
|
150
|
+
previous: options.force ? [] : this.state.layers,
|
|
151
|
+
meta: options.meta,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// when redirecting in browser
|
|
155
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
156
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.pushState(url, options.replace);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
protected async render(options: RouterRenderOptions = {}): Promise<void> {
|
|
164
|
+
const previous = options.previous ?? this.state.layers;
|
|
165
|
+
const url = options.url ?? this.url;
|
|
166
|
+
const start = this.dateTimeProvider.now();
|
|
167
|
+
|
|
168
|
+
this.transitioning = {
|
|
169
|
+
to: url,
|
|
170
|
+
from: this.state?.url.pathname,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
this.log.debug("Transitioning...", {
|
|
174
|
+
to: url,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const redirect = await this.router.transition(
|
|
178
|
+
new URL(`http://localhost${url}`),
|
|
179
|
+
previous,
|
|
180
|
+
options.meta,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (redirect) {
|
|
184
|
+
this.log.info("Redirecting to", {
|
|
185
|
+
redirect,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// if redirect is an absolute URL, use window.location.href (full page reload)
|
|
189
|
+
if (redirect.startsWith("http")) {
|
|
190
|
+
window.location.href = redirect;
|
|
191
|
+
} else {
|
|
192
|
+
// if redirect is a relative URL, use render() (single page app)
|
|
193
|
+
return await this.render({ url: redirect });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
198
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
199
|
+
|
|
200
|
+
this.transitioning = undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get embedded layers from the server.
|
|
205
|
+
*/
|
|
206
|
+
protected getHydrationState(): ReactHydrationState | undefined {
|
|
207
|
+
try {
|
|
208
|
+
if ("__ssr" in window && typeof window.__ssr === "object") {
|
|
209
|
+
return window.__ssr as ReactHydrationState;
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -------------------------------------------------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
protected readonly onTransitionEnd = $hook({
|
|
219
|
+
on: "react:transition:end",
|
|
220
|
+
handler: () => {
|
|
221
|
+
if (
|
|
222
|
+
this.options.scrollRestoration === "top" &&
|
|
223
|
+
typeof window !== "undefined" &&
|
|
224
|
+
!this.alepha.isTest()
|
|
225
|
+
) {
|
|
226
|
+
this.log.trace("Restoring scroll position to top");
|
|
227
|
+
window.scrollTo(0, 0);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
public readonly ready = $hook({
|
|
233
|
+
on: "ready",
|
|
234
|
+
handler: async () => {
|
|
235
|
+
const hydration = this.getHydrationState();
|
|
236
|
+
const previous = hydration?.layers ?? [];
|
|
237
|
+
|
|
238
|
+
if (hydration) {
|
|
239
|
+
// low budget, but works for now
|
|
240
|
+
for (const [key, value] of Object.entries(hydration)) {
|
|
241
|
+
if (key !== "layers") {
|
|
242
|
+
this.alepha.state.set(key as keyof State, value);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.render({ previous });
|
|
248
|
+
|
|
249
|
+
const element = this.router.root(this.state);
|
|
250
|
+
|
|
251
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
252
|
+
element,
|
|
253
|
+
root: this.getRootElement(),
|
|
254
|
+
hydration,
|
|
255
|
+
state: this.state,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
window.addEventListener("popstate", () => {
|
|
259
|
+
// when you update silently queryParams or hash, skip rendering
|
|
260
|
+
// if you want to force a rendering, use #go()
|
|
261
|
+
if (this.base + this.state.url.pathname === this.location.pathname) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.log.debug("Popstate event triggered - rendering new state", {
|
|
266
|
+
url: this.location.pathname + this.location.search,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.render();
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
});
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
269
276
|
|
|
270
277
|
export interface RouterGoOptions {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
replace?: boolean;
|
|
279
|
+
match?: TransitionOptions;
|
|
280
|
+
params?: Record<string, string>;
|
|
281
|
+
query?: Record<string, string>;
|
|
282
|
+
meta?: Record<string, any>;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Recreate the whole page, ignoring the current state.
|
|
286
|
+
*/
|
|
287
|
+
force?: boolean;
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
export type ReactHydrationState = {
|
|
284
|
-
|
|
291
|
+
layers?: Array<PreviousLayerData>;
|
|
285
292
|
} & {
|
|
286
|
-
|
|
293
|
+
[key: string]: any;
|
|
287
294
|
};
|
|
288
295
|
|
|
289
296
|
export interface RouterRenderOptions {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
url?: string;
|
|
298
|
+
previous?: PreviousLayerData[];
|
|
299
|
+
meta?: Record<string, any>;
|
|
293
300
|
}
|
|
@@ -3,20 +3,20 @@ import { $logger } from "@alepha/logger";
|
|
|
3
3
|
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
4
4
|
|
|
5
5
|
export class ReactBrowserRendererProvider {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
protected readonly log = $logger();
|
|
7
|
+
protected root?: Root;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
protected readonly onBrowserRender = $hook({
|
|
10
|
+
on: "react:browser:render",
|
|
11
|
+
handler: async ({ hydration, root, element }) => {
|
|
12
|
+
if (hydration?.layers) {
|
|
13
|
+
this.root = hydrateRoot(root, element);
|
|
14
|
+
this.log.info("Hydrated root element");
|
|
15
|
+
} else {
|
|
16
|
+
this.root ??= createRoot(root);
|
|
17
|
+
this.root.render(element);
|
|
18
|
+
this.log.info("Created root element");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
22
|
}
|