@alepha/react 0.6.2 → 0.6.3

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.
@@ -1,41 +1,47 @@
1
- import { __descriptor, KIND, NotImplementedError, EventEmitter, $logger, $inject, Alepha, $hook, t } from '@alepha/core';
2
1
  import { jsx } from 'react/jsx-runtime';
3
2
  import React, { createContext, useContext, useState, useEffect, createElement, useMemo } from 'react';
4
3
  import { HttpClient } from '@alepha/server';
4
+ import { __descriptor, NotImplementedError, KIND, $logger, $inject, Alepha, EventEmitter, $hook, t } from '@alepha/core';
5
5
  import { hydrateRoot, createRoot } from 'react-dom/client';
6
- import { compile, match } from 'path-to-regexp';
6
+ import { RouterProvider } from '@alepha/router';
7
7
 
8
- const KEY = "AUTH";
9
- const $auth = (options) => {
8
+ const KEY = "PAGE";
9
+ const $page = (options) => {
10
10
  __descriptor(KEY);
11
- return {
12
- [KIND]: KEY,
13
- options,
14
- jwks: () => {
15
- return options.oidc?.issuer ?? "";
11
+ if (options.children) {
12
+ for (const child of options.children) {
13
+ child.options.parent = {
14
+ options
15
+ };
16
16
  }
17
- };
18
- };
19
- $auth[KIND] = KEY;
20
-
21
- const pageDescriptorKey = "PAGE";
22
- const $page = (options) => {
23
- __descriptor(pageDescriptorKey);
17
+ }
18
+ if (options.parent) {
19
+ options.parent.options.children ??= [];
20
+ options.parent.options.children.push({
21
+ options
22
+ });
23
+ }
24
24
  return {
25
- [KIND]: pageDescriptorKey,
25
+ [KIND]: KEY,
26
26
  options,
27
27
  render: () => {
28
- throw new NotImplementedError(pageDescriptorKey);
28
+ throw new NotImplementedError(KEY);
29
29
  },
30
30
  go: () => {
31
- throw new NotImplementedError(pageDescriptorKey);
31
+ throw new NotImplementedError(KEY);
32
32
  },
33
33
  createAnchorProps: () => {
34
- throw new NotImplementedError(pageDescriptorKey);
34
+ throw new NotImplementedError(KEY);
35
+ },
36
+ can: () => {
37
+ if (options.can) {
38
+ return options.can();
39
+ }
40
+ return true;
35
41
  }
36
42
  };
37
43
  };
38
- $page[KIND] = pageDescriptorKey;
44
+ $page[KIND] = KEY;
39
45
 
40
46
  const RouterContext = createContext(
41
47
  void 0
@@ -52,7 +58,7 @@ const NestedView = (props) => {
52
58
  );
53
59
  useEffect(() => {
54
60
  if (app?.alepha.isBrowser()) {
55
- return app?.router.on("end", (state) => {
61
+ return app?.events.on("end", (state) => {
56
62
  setView(state.layers[index]?.element);
57
63
  });
58
64
  }
@@ -67,159 +73,37 @@ class RedirectionError extends Error {
67
73
  }
68
74
  }
69
75
 
70
- class Router extends EventEmitter {
76
+ class PageDescriptorProvider {
71
77
  log = $logger();
72
78
  alepha = $inject(Alepha);
73
79
  pages = [];
74
- notFoundPageRoute;
75
- /**
76
- * Get the page by name.
77
- *
78
- * @param name - Page name
79
- * @return PageRoute
80
- */
80
+ getPages() {
81
+ return this.pages;
82
+ }
81
83
  page(name) {
82
- const found = this.pages.find((it) => it.name === name);
83
- if (!found) {
84
- throw new Error(`Page ${name} not found`);
84
+ for (const page of this.pages) {
85
+ if (page.name === name) {
86
+ return page;
87
+ }
85
88
  }
86
- return found;
89
+ throw new Error(`Page ${name} not found`);
87
90
  }
88
- /**
89
- *
90
- */
91
- root(state, context = {}) {
91
+ root(state, context = {}, events) {
92
92
  return createElement(
93
93
  RouterContext.Provider,
94
94
  {
95
95
  value: {
96
- state,
97
- router: this,
98
96
  alepha: this.alepha,
99
- args: context
97
+ state,
98
+ context,
99
+ events: events ?? new EventEmitter()
100
100
  }
101
101
  },
102
- state.layers[0]?.element
102
+ createElement(NestedView, {}, state.layers[0]?.element)
103
103
  );
104
104
  }
105
- /**
106
- *
107
- * @param url
108
- * @param options
109
- */
110
- async render(url, options = {}) {
111
- const [pathname, search = ""] = url.split("?");
112
- const state = {
113
- pathname,
114
- search,
115
- layers: [],
116
- context: {}
117
- };
118
- await this.emit("begin", void 0);
119
- try {
120
- let layers = await this.match(url, options, state.context);
121
- if (layers.length === 0) {
122
- if (this.notFoundPageRoute) {
123
- layers = await this.createLayers(url, this.notFoundPageRoute);
124
- } else {
125
- layers.push({
126
- name: "not-found",
127
- element: "Not Found",
128
- index: 0,
129
- path: "/"
130
- });
131
- }
132
- }
133
- state.layers = layers;
134
- await this.emit("success", void 0);
135
- } catch (e) {
136
- if (e instanceof RedirectionError) {
137
- return {
138
- element: null,
139
- layers: [],
140
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
141
- context: state.context
142
- };
143
- }
144
- this.log.error(e);
145
- state.layers = [
146
- {
147
- name: "error",
148
- element: this.renderError(e),
149
- index: 0,
150
- path: "/"
151
- }
152
- ];
153
- await this.emit("error", e);
154
- }
155
- if (options.state) {
156
- options.state.layers = state.layers;
157
- options.state.pathname = state.pathname;
158
- options.state.search = state.search;
159
- options.state.context = state.context;
160
- await this.emit("end", options.state);
161
- return {
162
- element: this.root(options.state, options.args),
163
- layers: options.state.layers,
164
- context: state.context
165
- };
166
- }
167
- await this.emit("end", state);
168
- return {
169
- element: this.root(state, options.args),
170
- layers: state.layers,
171
- context: state.context
172
- };
173
- }
174
- /**
175
- *
176
- * @param url
177
- * @param options
178
- * @param context
179
- * @protected
180
- */
181
- async match(url, options = {}, context = {}) {
182
- const pages = this.pages;
183
- const previous = options.previous;
184
- const [pathname, search] = url.split("?");
185
- for (const route of pages) {
186
- if (route.children?.find((it) => !it.path || it.path === "/")) continue;
187
- if (!route.match) continue;
188
- const match2 = route.match.exec(pathname);
189
- if (match2) {
190
- const params = match2.params ?? {};
191
- const query = {};
192
- if (search) {
193
- for (const [key, value] of new URLSearchParams(search).entries()) {
194
- query[key] = String(value);
195
- }
196
- }
197
- return await this.createLayers(
198
- url,
199
- route,
200
- params,
201
- query,
202
- previous,
203
- options.args,
204
- context
205
- );
206
- }
207
- }
208
- return [];
209
- }
210
- /**
211
- * Create layers for the given route.
212
- *
213
- * @param url
214
- * @param route
215
- * @param params
216
- * @param query
217
- * @param previous
218
- * @param args
219
- * @param renderContext
220
- * @protected
221
- */
222
- async createLayers(url, route, params = {}, query = {}, previous = [], args, renderContext) {
105
+ async createLayers(route, request) {
106
+ const { pathname, search } = request.url;
223
107
  const layers = [];
224
108
  let context = {};
225
109
  const stack = [{ route }];
@@ -234,13 +118,13 @@ class Router extends EventEmitter {
234
118
  const route2 = it.route;
235
119
  const config = {};
236
120
  try {
237
- config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : query;
121
+ config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, request.query) : request.query;
238
122
  } catch (e) {
239
123
  it.error = e;
240
124
  break;
241
125
  }
242
126
  try {
243
- config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : params;
127
+ config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, request.params) : request.params;
244
128
  } catch (e) {
245
129
  it.error = e;
246
130
  break;
@@ -251,14 +135,15 @@ class Router extends EventEmitter {
251
135
  if (!route2.resolve) {
252
136
  continue;
253
137
  }
138
+ const previous = request.previous;
254
139
  if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
255
- const url2 = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
140
+ const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
256
141
  const prev = JSON.stringify({
257
- part: url2(previous[i].part),
142
+ part: url(previous[i].part),
258
143
  params: previous[i].config?.params ?? {}
259
144
  });
260
145
  const curr = JSON.stringify({
261
- part: url2(route2.path),
146
+ part: url(route2.path),
262
147
  params: config.params ?? {}
263
148
  });
264
149
  if (prev === curr) {
@@ -273,15 +158,14 @@ class Router extends EventEmitter {
273
158
  forceRefresh = true;
274
159
  }
275
160
  try {
276
- const props = await route2.resolve?.(
277
- {
278
- ...config,
279
- ...context,
280
- context: args,
281
- url
282
- },
283
- args ?? {}
284
- ) ?? {};
161
+ const props = await route2.resolve?.({
162
+ ...request,
163
+ // request
164
+ ...config,
165
+ // params, query
166
+ ...context
167
+ // previous props
168
+ }) ?? {};
285
169
  it.props = {
286
170
  ...props
287
171
  };
@@ -291,7 +175,13 @@ class Router extends EventEmitter {
291
175
  };
292
176
  } catch (e) {
293
177
  if (e instanceof RedirectionError) {
294
- throw e;
178
+ return {
179
+ layers: [],
180
+ redirect: typeof e.page === "string" ? e.page : this.href(e.page),
181
+ head: request.head,
182
+ pathname,
183
+ search
184
+ };
295
185
  }
296
186
  this.log.error(e);
297
187
  it.error = e;
@@ -302,25 +192,25 @@ class Router extends EventEmitter {
302
192
  for (let i = 0; i < stack.length; i++) {
303
193
  const it = stack[i];
304
194
  const props = it.props ?? {};
305
- const params2 = { ...it.config?.params };
306
- for (const key of Object.keys(params2)) {
307
- params2[key] = String(params2[key]);
195
+ const params = { ...it.config?.params };
196
+ for (const key of Object.keys(params)) {
197
+ params[key] = String(params[key]);
308
198
  }
309
- if (it.route.head && renderContext && !it.error) {
310
- this.mergeRenderContext(it.route, renderContext, {
199
+ if (it.route.head && !it.error) {
200
+ this.fillHead(it.route, request, {
311
201
  ...props,
312
202
  ...context
313
203
  });
314
204
  }
315
205
  acc += "/";
316
- acc += it.route.path ? compile(it.route.path)(params2) : "";
206
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
317
207
  const path = acc.replace(/\/+/, "/");
318
208
  if (it.error) {
319
209
  const errorHandler = this.getErrorHandler(it.route);
320
210
  const element = await (errorHandler ? errorHandler({
321
211
  ...it.config,
322
212
  error: it.error,
323
- url
213
+ url: ""
324
214
  }) : this.renderError(it.error));
325
215
  layers.push({
326
216
  props,
@@ -348,13 +238,8 @@ class Router extends EventEmitter {
348
238
  path
349
239
  });
350
240
  }
351
- return layers;
241
+ return { layers, head: request.head, pathname, search };
352
242
  }
353
- /**
354
- *
355
- * @param route
356
- * @protected
357
- */
358
243
  getErrorHandler(route) {
359
244
  if (route.errorHandler) return route.errorHandler;
360
245
  let parent = route.parent;
@@ -363,12 +248,6 @@ class Router extends EventEmitter {
363
248
  parent = parent.parent;
364
249
  }
365
250
  }
366
- /**
367
- *
368
- * @param page
369
- * @param props
370
- * @protected
371
- */
372
251
  async createElement(page, props) {
373
252
  if (page.lazy) {
374
253
  const component = await page.lazy();
@@ -379,65 +258,43 @@ class Router extends EventEmitter {
379
258
  }
380
259
  return void 0;
381
260
  }
382
- /**
383
- * Merge the render context with the page context.
384
- *
385
- * @param page
386
- * @param ctx
387
- * @param props
388
- * @protected
389
- */
390
- mergeRenderContext(page, ctx, props) {
391
- if (page.head) {
261
+ fillHead(page, ctx, props) {
262
+ if (!page.head) {
263
+ return;
264
+ }
265
+ ctx.head ??= {};
266
+ const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
267
+ if (head.title) {
392
268
  ctx.head ??= {};
393
- const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
394
- if (head.title) {
395
- ctx.head ??= {};
396
- if (ctx.head.titleSeparator) {
397
- ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
398
- } else {
399
- ctx.head.title = head.title;
400
- }
401
- ctx.head.titleSeparator = head.titleSeparator;
402
- }
403
- if (head.htmlAttributes) {
404
- ctx.head.htmlAttributes = {
405
- ...ctx.head.htmlAttributes,
406
- ...head.htmlAttributes
407
- };
408
- }
409
- if (head.bodyAttributes) {
410
- ctx.head.bodyAttributes = {
411
- ...ctx.head.bodyAttributes,
412
- ...head.bodyAttributes
413
- };
414
- }
415
- if (head.meta) {
416
- ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
269
+ if (ctx.head.titleSeparator) {
270
+ ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
271
+ } else {
272
+ ctx.head.title = head.title;
417
273
  }
274
+ ctx.head.titleSeparator = head.titleSeparator;
275
+ }
276
+ if (head.htmlAttributes) {
277
+ ctx.head.htmlAttributes = {
278
+ ...ctx.head.htmlAttributes,
279
+ ...head.htmlAttributes
280
+ };
281
+ }
282
+ if (head.bodyAttributes) {
283
+ ctx.head.bodyAttributes = {
284
+ ...ctx.head.bodyAttributes,
285
+ ...head.bodyAttributes
286
+ };
287
+ }
288
+ if (head.meta) {
289
+ ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
418
290
  }
419
291
  }
420
- /**
421
- *
422
- * @param e
423
- * @protected
424
- */
425
292
  renderError(e) {
426
293
  return createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
427
294
  }
428
- /**
429
- * Render an empty view.
430
- *
431
- * @protected
432
- */
433
295
  renderEmptyView() {
434
296
  return createElement(NestedView, {});
435
297
  }
436
- /**
437
- * Create a valid href for the given page.
438
- * @param page
439
- * @param params
440
- */
441
298
  href(page, params = {}) {
442
299
  const found = this.pages.find((it) => it.name === page.options.name);
443
300
  if (!found) {
@@ -449,16 +306,15 @@ class Router extends EventEmitter {
449
306
  url = `${parent.path ?? ""}/${url}`;
450
307
  parent = parent.parent;
451
308
  }
452
- url = compile(url)(params);
309
+ url = this.compile(url, params);
453
310
  return url.replace(/\/\/+/g, "/") || "/";
454
311
  }
455
- /**
456
- *
457
- * @param index
458
- * @param path
459
- * @param view
460
- * @protected
461
- */
312
+ compile(path, params = {}) {
313
+ for (const [key, value] of Object.entries(params)) {
314
+ path = path.replace(`:${key}`, value);
315
+ }
316
+ return path;
317
+ }
462
318
  renderView(index, path, view = this.renderEmptyView()) {
463
319
  return createElement(
464
320
  RouterLayerContext.Provider,
@@ -471,23 +327,39 @@ class Router extends EventEmitter {
471
327
  view
472
328
  );
473
329
  }
474
- /**
475
- *
476
- * @param entry
477
- */
330
+ configure = $hook({
331
+ name: "configure",
332
+ handler: () => {
333
+ const pages = this.alepha.getDescriptorValues($page);
334
+ for (const { value, key } of pages) {
335
+ value.options.name ??= key;
336
+ if (value.options.parent) {
337
+ continue;
338
+ }
339
+ this.add(this.map(pages, value));
340
+ }
341
+ }
342
+ });
343
+ map(pages, target) {
344
+ const children = target.options.children ?? [];
345
+ for (const it of pages) {
346
+ if (it.value.options.parent === target) {
347
+ children.push(it.value);
348
+ }
349
+ }
350
+ return {
351
+ ...target.options,
352
+ parent: void 0,
353
+ children: children.map((it) => this.map(pages, it))
354
+ };
355
+ }
478
356
  add(entry) {
479
357
  if (this.alepha.isReady()) {
480
358
  throw new Error("Router is already initialized");
481
359
  }
482
- if (entry.notFoundHandler) {
483
- this.notFoundPageRoute = {
484
- name: "not-found",
485
- component: entry.notFoundHandler
486
- };
487
- }
488
360
  entry.name ??= this.nextId();
489
361
  const page = entry;
490
- page.match = this.createMatchFunction(page);
362
+ page.match = this.createMatch(page);
491
363
  this.pages.push(page);
492
364
  if (page.children) {
493
365
  for (const child of page.children) {
@@ -496,13 +368,7 @@ class Router extends EventEmitter {
496
368
  }
497
369
  }
498
370
  }
499
- /**
500
- * Create a match function for the given page.
501
- *
502
- * @param page
503
- * @protected
504
- */
505
- createMatchFunction(page) {
371
+ createMatch(page) {
506
372
  let url = page.path ?? "/";
507
373
  let target = page.parent;
508
374
  while (target) {
@@ -510,76 +376,166 @@ class Router extends EventEmitter {
510
376
  target = target.parent;
511
377
  }
512
378
  let path = url.replace(/\/\/+/g, "/");
513
- if (path.endsWith("/")) {
379
+ if (path.endsWith("/") && path !== "/") {
514
380
  path = path.slice(0, -1);
515
381
  }
516
- if (path.includes("?")) {
517
- return {
518
- exec: match(path.split("?")[0]),
519
- path
520
- };
521
- }
522
- return {
523
- exec: match(path),
524
- path
525
- };
382
+ return path;
526
383
  }
527
- /**
528
- *
529
- */
530
- empty() {
531
- return this.pages.length === 0;
532
- }
533
- /**
534
- *
535
- * @protected
536
- */
537
384
  _next = 0;
538
- /**
539
- *
540
- * @protected
541
- */
542
385
  nextId() {
543
386
  this._next += 1;
544
387
  return `P${this._next}`;
545
388
  }
546
389
  }
390
+ const isPageRoute = (it) => {
391
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
392
+ };
547
393
 
548
- class PageDescriptorProvider {
394
+ class BrowserHeadProvider {
395
+ renderHead(document, head) {
396
+ if (head.title) {
397
+ document.title = head.title;
398
+ }
399
+ if (head.bodyAttributes) {
400
+ for (const [key, value] of Object.entries(head.bodyAttributes)) {
401
+ if (value) {
402
+ document.body.setAttribute(key, value);
403
+ } else {
404
+ document.body.removeAttribute(key);
405
+ }
406
+ }
407
+ }
408
+ if (head.htmlAttributes) {
409
+ for (const [key, value] of Object.entries(head.htmlAttributes)) {
410
+ if (value) {
411
+ document.documentElement.setAttribute(key, value);
412
+ } else {
413
+ document.documentElement.removeAttribute(key);
414
+ }
415
+ }
416
+ }
417
+ if (head.meta) {
418
+ for (const [key, value] of Object.entries(head.meta)) {
419
+ const meta = document.querySelector(`meta[name="${key}"]`);
420
+ if (meta) {
421
+ meta.setAttribute("content", value.content);
422
+ } else {
423
+ const newMeta = document.createElement("meta");
424
+ newMeta.setAttribute("name", key);
425
+ newMeta.setAttribute("content", value.content);
426
+ document.head.appendChild(newMeta);
427
+ }
428
+ }
429
+ }
430
+ }
431
+ }
432
+
433
+ class BrowserRouterProvider extends RouterProvider {
434
+ log = $logger();
549
435
  alepha = $inject(Alepha);
550
- router = $inject(Router);
436
+ pageDescriptorProvider = $inject(PageDescriptorProvider);
437
+ events = new EventEmitter();
438
+ add(entry) {
439
+ this.pageDescriptorProvider.add(entry);
440
+ }
551
441
  configure = $hook({
552
442
  name: "configure",
553
- handler: () => {
554
- const pages = this.alepha.getDescriptorValues($page);
555
- for (const { value, key } of pages) {
556
- value.options.name ??= key;
557
- if (pages.find((it) => it.value.options.children?.().includes(value))) {
558
- continue;
443
+ handler: async () => {
444
+ for (const page of this.pageDescriptorProvider.getPages()) {
445
+ if (page.component || page.lazy) {
446
+ this.push({
447
+ path: page.match,
448
+ page
449
+ });
559
450
  }
560
- this.router.add(this.map(pages, value));
561
451
  }
562
452
  }
563
453
  });
564
- /**
565
- * Transform
566
- * @param pages
567
- * @param target
568
- * @protected
569
- */
570
- map(pages, target) {
571
- const children = target.options.children?.() ?? [];
572
- for (const it of pages) {
573
- if (it.value.options.parent === target) {
574
- children.push(it.value);
454
+ async transition(url, options = {}) {
455
+ const { pathname, search } = url;
456
+ const state = {
457
+ pathname,
458
+ search,
459
+ layers: [],
460
+ head: {}
461
+ };
462
+ await this.events.emit("begin", void 0);
463
+ try {
464
+ const previous = options.previous;
465
+ const { route, params } = this.match(pathname);
466
+ const query = {};
467
+ if (search) {
468
+ for (const [key, value] of new URLSearchParams(search).entries()) {
469
+ query[key] = String(value);
470
+ }
575
471
  }
472
+ if (isPageRoute(route)) {
473
+ const result = await this.pageDescriptorProvider.createLayers(
474
+ route.page,
475
+ {
476
+ url,
477
+ params: params ?? {},
478
+ query,
479
+ previous,
480
+ ...state,
481
+ head: state.head,
482
+ ...options.context ?? {}
483
+ }
484
+ );
485
+ if (result.redirect) {
486
+ return {
487
+ element: null,
488
+ layers: [],
489
+ redirect: result.redirect,
490
+ head: state.head
491
+ };
492
+ }
493
+ state.layers = result.layers;
494
+ state.head = result.head;
495
+ }
496
+ if (state.layers.length === 0) {
497
+ state.layers.push({
498
+ name: "not-found",
499
+ element: "Not Found",
500
+ index: 0,
501
+ path: "/"
502
+ });
503
+ }
504
+ await this.events.emit("success", void 0);
505
+ } catch (e) {
506
+ this.log.error(e);
507
+ state.layers = [
508
+ {
509
+ name: "error",
510
+ element: this.pageDescriptorProvider.renderError(e),
511
+ index: 0,
512
+ path: "/"
513
+ }
514
+ ];
515
+ await this.events.emit("error", e);
576
516
  }
517
+ if (!options.state) {
518
+ await this.events.emit("end", state);
519
+ return {
520
+ element: this.root(state, options.context),
521
+ layers: state.layers,
522
+ head: state.head
523
+ };
524
+ }
525
+ options.state.layers = state.layers;
526
+ options.state.pathname = state.pathname;
527
+ options.state.search = state.search;
528
+ options.state.head = state.head;
529
+ await this.events.emit("end", options.state);
577
530
  return {
578
- ...target.options,
579
- parent: void 0,
580
- children: children.map((it) => this.map(pages, it))
531
+ element: this.root(state, options.context),
532
+ layers: options.state.layers,
533
+ head: state.head
581
534
  };
582
535
  }
536
+ root(state, context = {}) {
537
+ return this.pageDescriptorProvider.root(state, context, this.events);
538
+ }
583
539
  }
584
540
 
585
541
  const envSchema = t.object({
@@ -588,7 +544,9 @@ const envSchema = t.object({
588
544
  class ReactBrowserProvider {
589
545
  log = $logger();
590
546
  client = $inject(HttpClient);
591
- router = $inject(Router);
547
+ alepha = $inject(Alepha);
548
+ router = $inject(BrowserRouterProvider);
549
+ headProvider = $inject(BrowserHeadProvider);
592
550
  env = $inject(envSchema);
593
551
  root;
594
552
  transitioning;
@@ -596,30 +554,17 @@ class ReactBrowserProvider {
596
554
  layers: [],
597
555
  pathname: "",
598
556
  search: "",
599
- context: {}
557
+ head: {}
600
558
  };
601
- /**
602
- *
603
- */
604
559
  get document() {
605
560
  return window.document;
606
561
  }
607
- /**
608
- *
609
- */
610
562
  get history() {
611
563
  return window.history;
612
564
  }
613
- /**
614
- *
615
- */
616
565
  get url() {
617
566
  return window.location.pathname + window.location.search;
618
567
  }
619
- /**
620
- *
621
- * @param props
622
- */
623
568
  async invalidate(props) {
624
569
  const previous = [];
625
570
  if (props) {
@@ -660,66 +605,22 @@ class ReactBrowserProvider {
660
605
  }
661
606
  this.history.pushState({}, "", url);
662
607
  }
663
- /**
664
- *
665
- * @param options
666
- * @protected
667
- */
668
608
  async render(options = {}) {
669
609
  const previous = options.previous ?? this.state.layers;
670
610
  const url = options.url ?? this.url;
671
611
  this.transitioning = { to: url };
672
- const result = await this.router.render(url, {
673
- previous,
674
- state: this.state
675
- });
612
+ const result = await this.router.transition(
613
+ new URL(`http://localhost${url}`),
614
+ {
615
+ previous,
616
+ state: this.state
617
+ }
618
+ );
676
619
  if (result.redirect) {
677
620
  return await this.render({ url: result.redirect });
678
621
  }
679
622
  this.transitioning = void 0;
680
- return { url, context: result.context };
681
- }
682
- /**
683
- * Render the helmet context.
684
- *
685
- * @param ctx
686
- * @protected
687
- */
688
- renderHeadContext(ctx) {
689
- if (ctx.title) {
690
- this.document.title = ctx.title;
691
- }
692
- if (ctx.bodyAttributes) {
693
- for (const [key, value] of Object.entries(ctx.bodyAttributes)) {
694
- if (value) {
695
- this.document.body.setAttribute(key, value);
696
- } else {
697
- this.document.body.removeAttribute(key);
698
- }
699
- }
700
- }
701
- if (ctx.htmlAttributes) {
702
- for (const [key, value] of Object.entries(ctx.htmlAttributes)) {
703
- if (value) {
704
- this.document.documentElement.setAttribute(key, value);
705
- } else {
706
- this.document.documentElement.removeAttribute(key);
707
- }
708
- }
709
- }
710
- if (ctx.meta) {
711
- for (const [key, value] of Object.entries(ctx.meta)) {
712
- const meta = this.document.querySelector(`meta[name="${key}"]`);
713
- if (meta) {
714
- meta.setAttribute("content", value.content);
715
- } else {
716
- const newMeta = this.document.createElement("meta");
717
- newMeta.setAttribute("name", key);
718
- newMeta.setAttribute("content", value.content);
719
- this.document.head.appendChild(newMeta);
720
- }
721
- }
722
- }
623
+ return { url, head: result.head };
723
624
  }
724
625
  /**
725
626
  * Get embedded layers from the server.
@@ -749,18 +650,6 @@ class ReactBrowserProvider {
749
650
  this.document.body.prepend(div);
750
651
  return div;
751
652
  }
752
- getUserFromCookies() {
753
- const cookies = this.document.cookie.split("; ");
754
- const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
755
- try {
756
- if (userCookie) {
757
- return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
758
- }
759
- } catch (error) {
760
- this.log.warn(error, "Failed to parse user cookie");
761
- }
762
- return void 0;
763
- }
764
653
  // -------------------------------------------------------------------------------------------------------------------
765
654
  /**
766
655
  *
@@ -774,13 +663,16 @@ class ReactBrowserProvider {
774
663
  if (cache?.links) {
775
664
  this.client.links = cache.links;
776
665
  }
777
- const { context } = await this.render({ previous });
778
- if (context.head) {
779
- this.renderHeadContext(context.head);
666
+ const { head } = await this.render({ previous });
667
+ if (head) {
668
+ this.headProvider.renderHead(this.document, head);
780
669
  }
781
- const element = this.router.root(this.state, {
782
- user: cache?.user ?? this.getUserFromCookies()
670
+ const context = {};
671
+ await this.alepha.run("react:browser:render", {
672
+ context,
673
+ cache
783
674
  });
675
+ const element = this.router.root(this.state, context);
784
676
  if (previous.length > 0) {
785
677
  this.root = hydrateRoot(this.getRootElement(), element);
786
678
  this.log.info("Hydrated root element");
@@ -792,50 +684,13 @@ class ReactBrowserProvider {
792
684
  window.addEventListener("popstate", () => {
793
685
  this.render();
794
686
  });
795
- this.router.on("end", ({ context: context2 }) => {
796
- if (context2.head) {
797
- this.renderHeadContext(context2.head);
798
- }
687
+ this.router.events.on("end", ({ head: head2 }) => {
688
+ this.headProvider.renderHead(this.document, head2);
799
689
  });
800
690
  }
801
691
  });
802
692
  }
803
693
 
804
- class Auth {
805
- alepha = $inject(Alepha);
806
- log = $logger();
807
- client = $inject(HttpClient);
808
- slugs = {
809
- login: "/api/_oauth/login",
810
- logout: "/api/_oauth/logout"
811
- };
812
- start = $hook({
813
- name: "start",
814
- handler: async () => {
815
- this.client.on("onError", (err) => {
816
- if (err.statusCode === 401) {
817
- this.login();
818
- }
819
- });
820
- }
821
- });
822
- login = (provider) => {
823
- if (this.alepha.isBrowser()) {
824
- const browser = this.alepha.get(ReactBrowserProvider);
825
- const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
826
- window.location.href = `${this.slugs.login}?redirect=${redirect}`;
827
- if (browser.transitioning) {
828
- throw new RedirectionError(browser.state.pathname);
829
- }
830
- return;
831
- }
832
- throw new RedirectionError(this.slugs.login);
833
- };
834
- logout = () => {
835
- window.location.href = `${this.slugs.logout}?redirect=${encodeURIComponent(window.location.origin)}`;
836
- };
837
- }
838
-
839
694
  class RouterHookApi {
840
695
  constructor(state, layer, browser) {
841
696
  this.state = state;
@@ -956,7 +811,7 @@ const useRouter = () => {
956
811
  layer,
957
812
  ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
958
813
  ),
959
- [ctx.router, layer]
814
+ [layer]
960
815
  );
961
816
  };
962
817
 
@@ -968,7 +823,6 @@ const Link = (props) => {
968
823
  }
969
824
  const can = typeof props.to === "string" ? void 0 : props.to.options.can;
970
825
  if (can && !can()) {
971
- console.log("I cannot go to", to);
972
826
  return null;
973
827
  }
974
828
  const name = typeof props.to === "string" ? void 0 : props.to.options.name;
@@ -1040,13 +894,13 @@ const useRouterEvents = (opts = {}) => {
1040
894
  const onEnd = opts.onEnd;
1041
895
  const onError = opts.onError;
1042
896
  if (onBegin) {
1043
- subs.push(ctx.router.on("begin", onBegin));
897
+ subs.push(ctx.events.on("begin", onBegin));
1044
898
  }
1045
899
  if (onEnd) {
1046
- subs.push(ctx.router.on("end", onEnd));
900
+ subs.push(ctx.events.on("end", onEnd));
1047
901
  }
1048
902
  if (onError) {
1049
- subs.push(ctx.router.on("error", onError));
903
+ subs.push(ctx.events.on("error", onError));
1050
904
  }
1051
905
  return () => {
1052
906
  for (const sub of subs) {
@@ -1064,7 +918,7 @@ const useRouterState = () => {
1064
918
  }
1065
919
  const [state, setState] = useState(ctx.state);
1066
920
  useEffect(
1067
- () => ctx.router.on("end", (it) => {
921
+ () => ctx.events.on("end", (it) => {
1068
922
  setState({ ...it });
1069
923
  }),
1070
924
  []
@@ -1088,7 +942,7 @@ const useActive = (path) => {
1088
942
  const [isPending, setPending] = useState(false);
1089
943
  const isActive = current === href;
1090
944
  useEffect(
1091
- () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
945
+ () => ctx.events.on("end", ({ pathname }) => setCurrent(pathname)),
1092
946
  []
1093
947
  );
1094
948
  return {
@@ -1111,21 +965,4 @@ const useActive = (path) => {
1111
965
  };
1112
966
  };
1113
967
 
1114
- const useAuth = () => {
1115
- const ctx = useContext(RouterContext);
1116
- if (!ctx) {
1117
- throw new Error("useAuth must be used within a RouterContext");
1118
- }
1119
- const args = ctx.args ?? {};
1120
- return {
1121
- user: args.user,
1122
- logout: () => {
1123
- ctx.alepha.get(Auth).logout();
1124
- },
1125
- login: (provider) => {
1126
- ctx.alepha.get(Auth).login();
1127
- }
1128
- };
1129
- };
1130
-
1131
- export { $auth as $, Auth as A, Link as L, NestedView as N, PageDescriptorProvider as P, Router as R, $page as a, RouterContext as b, RouterLayerContext as c, RouterHookApi as d, useClient as e, useQueryParams as f, useRouter as g, useRouterEvents as h, useRouterState as i, useActive as j, useAuth as k, ReactBrowserProvider as l, RedirectionError as m, pageDescriptorKey as p, useInject as u };
968
+ export { $page as $, BrowserRouterProvider as B, Link as L, NestedView as N, PageDescriptorProvider as P, RouterContext as R, RouterLayerContext as a, RouterHookApi as b, useClient as c, useQueryParams as d, useRouter as e, useRouterEvents as f, useRouterState as g, useActive as h, RedirectionError as i, isPageRoute as j, ReactBrowserProvider as k, useInject as u };