@alepha/react 0.6.1 → 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,38 +1,47 @@
1
- import { __descriptor, KIND, NotImplementedError, EventEmitter, $logger, $inject, Alepha, $hook } 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
+ if (options.children) {
12
+ for (const child of options.children) {
13
+ child.options.parent = {
14
+ options
15
+ };
16
+ }
17
+ }
18
+ if (options.parent) {
19
+ options.parent.options.children ??= [];
20
+ options.parent.options.children.push({
21
+ options
22
+ });
23
+ }
11
24
  return {
12
25
  [KIND]: KEY,
13
- options
14
- };
15
- };
16
- $auth[KIND] = KEY;
17
-
18
- const pageDescriptorKey = "PAGE";
19
- const $page = (options) => {
20
- __descriptor(pageDescriptorKey);
21
- return {
22
- [KIND]: pageDescriptorKey,
23
26
  options,
24
27
  render: () => {
25
- throw new NotImplementedError(pageDescriptorKey);
28
+ throw new NotImplementedError(KEY);
26
29
  },
27
30
  go: () => {
28
- throw new NotImplementedError(pageDescriptorKey);
31
+ throw new NotImplementedError(KEY);
29
32
  },
30
33
  createAnchorProps: () => {
31
- throw new NotImplementedError(pageDescriptorKey);
34
+ throw new NotImplementedError(KEY);
35
+ },
36
+ can: () => {
37
+ if (options.can) {
38
+ return options.can();
39
+ }
40
+ return true;
32
41
  }
33
42
  };
34
43
  };
35
- $page[KIND] = pageDescriptorKey;
44
+ $page[KIND] = KEY;
36
45
 
37
46
  const RouterContext = createContext(
38
47
  void 0
@@ -49,7 +58,7 @@ const NestedView = (props) => {
49
58
  );
50
59
  useEffect(() => {
51
60
  if (app?.alepha.isBrowser()) {
52
- return app?.router.on("end", (state) => {
61
+ return app?.events.on("end", (state) => {
53
62
  setView(state.layers[index]?.element);
54
63
  });
55
64
  }
@@ -64,162 +73,37 @@ class RedirectionError extends Error {
64
73
  }
65
74
  }
66
75
 
67
- class Router extends EventEmitter {
76
+ class PageDescriptorProvider {
68
77
  log = $logger();
69
78
  alepha = $inject(Alepha);
70
79
  pages = [];
71
- notFoundPageRoute;
72
- /**
73
- * Get the page by name.
74
- *
75
- * @param name - Page name
76
- * @return PageRoute
77
- */
80
+ getPages() {
81
+ return this.pages;
82
+ }
78
83
  page(name) {
79
- const found = this.pages.find((it) => it.name === name);
80
- if (!found) {
81
- throw new Error(`Page ${name} not found`);
84
+ for (const page of this.pages) {
85
+ if (page.name === name) {
86
+ return page;
87
+ }
82
88
  }
83
- return found;
89
+ throw new Error(`Page ${name} not found`);
84
90
  }
85
- /**
86
- *
87
- */
88
- root(state, context = {}) {
91
+ root(state, context = {}, events) {
89
92
  return createElement(
90
93
  RouterContext.Provider,
91
94
  {
92
95
  value: {
93
- state,
94
- router: this,
95
96
  alepha: this.alepha,
96
- args: {
97
- user: context.user,
98
- cookies: context.cookies
99
- }
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,18 +135,20 @@ 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) {
265
150
  it.props = previous[i].props;
151
+ it.error = previous[i].error;
266
152
  context = {
267
153
  ...context,
268
154
  ...it.props
@@ -272,15 +158,14 @@ class Router extends EventEmitter {
272
158
  forceRefresh = true;
273
159
  }
274
160
  try {
275
- const props = await route2.resolve?.(
276
- {
277
- ...config,
278
- ...context,
279
- context: args,
280
- url
281
- },
282
- args ?? {}
283
- ) ?? {};
161
+ const props = await route2.resolve?.({
162
+ ...request,
163
+ // request
164
+ ...config,
165
+ // params, query
166
+ ...context
167
+ // previous props
168
+ }) ?? {};
284
169
  it.props = {
285
170
  ...props
286
171
  };
@@ -290,7 +175,13 @@ class Router extends EventEmitter {
290
175
  };
291
176
  } catch (e) {
292
177
  if (e instanceof RedirectionError) {
293
- 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
+ };
294
185
  }
295
186
  this.log.error(e);
296
187
  it.error = e;
@@ -301,28 +192,29 @@ class Router extends EventEmitter {
301
192
  for (let i = 0; i < stack.length; i++) {
302
193
  const it = stack[i];
303
194
  const props = it.props ?? {};
304
- const params2 = { ...it.config?.params };
305
- for (const key of Object.keys(params2)) {
306
- 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]);
307
198
  }
308
- if (it.route.helmet && renderContext) {
309
- this.mergeRenderContext(it.route, renderContext, {
199
+ if (it.route.head && !it.error) {
200
+ this.fillHead(it.route, request, {
310
201
  ...props,
311
202
  ...context
312
203
  });
313
204
  }
314
205
  acc += "/";
315
- acc += it.route.path ? compile(it.route.path)(params2) : "";
206
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
316
207
  const path = acc.replace(/\/+/, "/");
317
208
  if (it.error) {
318
209
  const errorHandler = this.getErrorHandler(it.route);
319
- const element = errorHandler ? errorHandler({
210
+ const element = await (errorHandler ? errorHandler({
320
211
  ...it.config,
321
212
  error: it.error,
322
- url
323
- }) : this.renderError(it.error);
213
+ url: ""
214
+ }) : this.renderError(it.error));
324
215
  layers.push({
325
216
  props,
217
+ error: it.error,
326
218
  name: it.route.name,
327
219
  part: it.route.path,
328
220
  config: it.config,
@@ -346,13 +238,8 @@ class Router extends EventEmitter {
346
238
  path
347
239
  });
348
240
  }
349
- return layers;
241
+ return { layers, head: request.head, pathname, search };
350
242
  }
351
- /**
352
- *
353
- * @param route
354
- * @protected
355
- */
356
243
  getErrorHandler(route) {
357
244
  if (route.errorHandler) return route.errorHandler;
358
245
  let parent = route.parent;
@@ -361,12 +248,6 @@ class Router extends EventEmitter {
361
248
  parent = parent.parent;
362
249
  }
363
250
  }
364
- /**
365
- *
366
- * @param page
367
- * @param props
368
- * @protected
369
- */
370
251
  async createElement(page, props) {
371
252
  if (page.lazy) {
372
253
  const component = await page.lazy();
@@ -377,48 +258,43 @@ class Router extends EventEmitter {
377
258
  }
378
259
  return void 0;
379
260
  }
380
- /**
381
- * Merge the render context with the page context.
382
- *
383
- * @param page
384
- * @param ctx
385
- * @param props
386
- * @protected
387
- */
388
- mergeRenderContext(page, ctx, props) {
389
- if (page.helmet) {
390
- const helmet = typeof page.helmet === "function" ? page.helmet(props) : page.helmet;
391
- if (helmet.title) {
392
- ctx.helmet ??= {};
393
- if (ctx.helmet?.title) {
394
- ctx.helmet.title = `${helmet.title} - ${ctx.helmet.title}`;
395
- } else {
396
- ctx.helmet.title = helmet.title;
397
- }
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) {
268
+ ctx.head ??= {};
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;
398
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 ?? []];
399
290
  }
400
291
  }
401
- /**
402
- *
403
- * @param e
404
- * @protected
405
- */
406
292
  renderError(e) {
407
293
  return createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
408
294
  }
409
- /**
410
- * Render an empty view.
411
- *
412
- * @protected
413
- */
414
295
  renderEmptyView() {
415
296
  return createElement(NestedView, {});
416
297
  }
417
- /**
418
- * Create a valid href for the given page.
419
- * @param page
420
- * @param params
421
- */
422
298
  href(page, params = {}) {
423
299
  const found = this.pages.find((it) => it.name === page.options.name);
424
300
  if (!found) {
@@ -430,16 +306,15 @@ class Router extends EventEmitter {
430
306
  url = `${parent.path ?? ""}/${url}`;
431
307
  parent = parent.parent;
432
308
  }
433
- url = compile(url)(params);
309
+ url = this.compile(url, params);
434
310
  return url.replace(/\/\/+/g, "/") || "/";
435
311
  }
436
- /**
437
- *
438
- * @param index
439
- * @param path
440
- * @param view
441
- * @protected
442
- */
312
+ compile(path, params = {}) {
313
+ for (const [key, value] of Object.entries(params)) {
314
+ path = path.replace(`:${key}`, value);
315
+ }
316
+ return path;
317
+ }
443
318
  renderView(index, path, view = this.renderEmptyView()) {
444
319
  return createElement(
445
320
  RouterLayerContext.Provider,
@@ -452,23 +327,39 @@ class Router extends EventEmitter {
452
327
  view
453
328
  );
454
329
  }
455
- /**
456
- *
457
- * @param entry
458
- */
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
+ }
459
356
  add(entry) {
460
357
  if (this.alepha.isReady()) {
461
358
  throw new Error("Router is already initialized");
462
359
  }
463
- if (entry.notFoundHandler) {
464
- this.notFoundPageRoute = {
465
- name: "not-found",
466
- component: entry.notFoundHandler
467
- };
468
- }
469
360
  entry.name ??= this.nextId();
470
361
  const page = entry;
471
- page.match = this.createMatchFunction(page);
362
+ page.match = this.createMatch(page);
472
363
  this.pages.push(page);
473
364
  if (page.children) {
474
365
  for (const child of page.children) {
@@ -477,13 +368,7 @@ class Router extends EventEmitter {
477
368
  }
478
369
  }
479
370
  }
480
- /**
481
- * Create a match function for the given page.
482
- *
483
- * @param page
484
- * @protected
485
- */
486
- createMatchFunction(page) {
371
+ createMatch(page) {
487
372
  let url = page.path ?? "/";
488
373
  let target = page.parent;
489
374
  while (target) {
@@ -491,112 +376,195 @@ class Router extends EventEmitter {
491
376
  target = target.parent;
492
377
  }
493
378
  let path = url.replace(/\/\/+/g, "/");
494
- if (path.endsWith("/")) {
379
+ if (path.endsWith("/") && path !== "/") {
495
380
  path = path.slice(0, -1);
496
381
  }
497
- if (path.includes("?")) {
498
- return {
499
- exec: match(path.split("?")[0]),
500
- path
501
- };
502
- }
503
- return {
504
- exec: match(path),
505
- path
506
- };
507
- }
508
- /**
509
- *
510
- */
511
- empty() {
512
- return this.pages.length === 0;
382
+ return path;
513
383
  }
514
- /**
515
- *
516
- * @protected
517
- */
518
384
  _next = 0;
519
- /**
520
- *
521
- * @protected
522
- */
523
385
  nextId() {
524
386
  this._next += 1;
525
387
  return `P${this._next}`;
526
388
  }
527
389
  }
390
+ const isPageRoute = (it) => {
391
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
392
+ };
528
393
 
529
- 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();
530
435
  alepha = $inject(Alepha);
531
- router = $inject(Router);
436
+ pageDescriptorProvider = $inject(PageDescriptorProvider);
437
+ events = new EventEmitter();
438
+ add(entry) {
439
+ this.pageDescriptorProvider.add(entry);
440
+ }
532
441
  configure = $hook({
533
442
  name: "configure",
534
- handler: () => {
535
- const pages = this.alepha.getDescriptorValues($page);
536
- for (const { value, key } of pages) {
537
- value.options.name ??= key;
538
- if (pages.find((it) => it.value.options.children?.().includes(value))) {
539
- 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
+ });
540
450
  }
541
- this.router.add(this.map(pages, value));
542
451
  }
543
452
  }
544
453
  });
545
- /**
546
- * Transform
547
- * @param pages
548
- * @param target
549
- * @protected
550
- */
551
- map(pages, target) {
552
- const children = target.options.children?.() ?? [];
553
- for (const it of pages) {
554
- if (it.value.options.parent === target) {
555
- 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
+ }
556
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);
557
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);
558
530
  return {
559
- ...target.options,
560
- parent: void 0,
561
- children: children.map((it) => this.map(pages, it))
531
+ element: this.root(state, options.context),
532
+ layers: options.state.layers,
533
+ head: state.head
562
534
  };
563
535
  }
536
+ root(state, context = {}) {
537
+ return this.pageDescriptorProvider.root(state, context, this.events);
538
+ }
564
539
  }
565
540
 
541
+ const envSchema = t.object({
542
+ REACT_ROOT_ID: t.string({ default: "root" })
543
+ });
566
544
  class ReactBrowserProvider {
567
545
  log = $logger();
568
546
  client = $inject(HttpClient);
569
- router = $inject(Router);
547
+ alepha = $inject(Alepha);
548
+ router = $inject(BrowserRouterProvider);
549
+ headProvider = $inject(BrowserHeadProvider);
550
+ env = $inject(envSchema);
570
551
  root;
571
552
  transitioning;
572
553
  state = {
573
554
  layers: [],
574
555
  pathname: "",
575
556
  search: "",
576
- context: {}
557
+ head: {}
577
558
  };
578
- /**
579
- *
580
- */
581
559
  get document() {
582
560
  return window.document;
583
561
  }
584
- /**
585
- *
586
- */
587
562
  get history() {
588
563
  return window.history;
589
564
  }
590
- /**
591
- *
592
- */
593
565
  get url() {
594
566
  return window.location.pathname + window.location.search;
595
567
  }
596
- /**
597
- *
598
- * @param props
599
- */
600
568
  async invalidate(props) {
601
569
  const previous = [];
602
570
  if (props) {
@@ -637,29 +605,22 @@ class ReactBrowserProvider {
637
605
  }
638
606
  this.history.pushState({}, "", url);
639
607
  }
640
- /**
641
- *
642
- * @param options
643
- * @protected
644
- */
645
608
  async render(options = {}) {
646
609
  const previous = options.previous ?? this.state.layers;
647
610
  const url = options.url ?? this.url;
648
611
  this.transitioning = { to: url };
649
- const result = await this.router.render(url, {
650
- previous,
651
- state: this.state
652
- });
612
+ const result = await this.router.transition(
613
+ new URL(`http://localhost${url}`),
614
+ {
615
+ previous,
616
+ state: this.state
617
+ }
618
+ );
653
619
  if (result.redirect) {
654
620
  return await this.render({ url: result.redirect });
655
621
  }
656
622
  this.transitioning = void 0;
657
- return { url };
658
- }
659
- renderHelmetContext(ctx) {
660
- if (ctx.title) {
661
- this.document.title = ctx.title;
662
- }
623
+ return { url, head: result.head };
663
624
  }
664
625
  /**
665
626
  * Get embedded layers from the server.
@@ -680,27 +641,15 @@ class ReactBrowserProvider {
680
641
  * @protected
681
642
  */
682
643
  getRootElement() {
683
- const root = this.document.getElementById("root");
644
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
684
645
  if (root) {
685
646
  return root;
686
647
  }
687
648
  const div = this.document.createElement("div");
688
- div.id = "root";
689
- this.document.body.appendChild(div);
649
+ div.id = this.env.REACT_ROOT_ID;
650
+ this.document.body.prepend(div);
690
651
  return div;
691
652
  }
692
- getUserFromCookies() {
693
- const cookies = this.document.cookie.split("; ");
694
- const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
695
- try {
696
- if (userCookie) {
697
- return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
698
- }
699
- } catch (error) {
700
- this.log.warn(error, "Failed to parse user cookie");
701
- }
702
- return void 0;
703
- }
704
653
  // -------------------------------------------------------------------------------------------------------------------
705
654
  /**
706
655
  *
@@ -711,73 +660,35 @@ class ReactBrowserProvider {
711
660
  handler: async () => {
712
661
  const cache = this.getHydrationState();
713
662
  const previous = cache?.layers ?? [];
714
- await this.render({ previous });
715
- const element = this.router.root(this.state, {
716
- user: cache?.user ?? this.getUserFromCookies()
663
+ if (cache?.links) {
664
+ this.client.links = cache.links;
665
+ }
666
+ const { head } = await this.render({ previous });
667
+ if (head) {
668
+ this.headProvider.renderHead(this.document, head);
669
+ }
670
+ const context = {};
671
+ await this.alepha.run("react:browser:render", {
672
+ context,
673
+ cache
717
674
  });
675
+ const element = this.router.root(this.state, context);
718
676
  if (previous.length > 0) {
719
677
  this.root = hydrateRoot(this.getRootElement(), element);
720
678
  this.log.info("Hydrated root element");
721
679
  } else {
722
- this.root = createRoot(this.getRootElement());
680
+ this.root ??= createRoot(this.getRootElement());
723
681
  this.root.render(element);
724
682
  this.log.info("Created root element");
725
683
  }
726
684
  window.addEventListener("popstate", () => {
727
685
  this.render();
728
686
  });
729
- this.router.on("end", ({ context }) => {
730
- if (context.helmet) {
731
- this.renderHelmetContext(context.helmet);
732
- }
733
- });
734
- }
735
- });
736
- /**
737
- *
738
- * @protected
739
- */
740
- stop = $hook({
741
- name: "stop",
742
- handler: async () => {
743
- if (this.root) {
744
- this.root.unmount();
745
- this.log.info("Unmounted root element");
746
- }
747
- }
748
- });
749
- }
750
-
751
- class Auth {
752
- alepha = $inject(Alepha);
753
- log = $logger();
754
- client = $inject(HttpClient);
755
- api = "/api/_oauth/login";
756
- start = $hook({
757
- name: "start",
758
- handler: async () => {
759
- this.client.on("onError", (err) => {
760
- if (err.statusCode === 401) {
761
- this.login();
762
- }
687
+ this.router.events.on("end", ({ head: head2 }) => {
688
+ this.headProvider.renderHead(this.document, head2);
763
689
  });
764
690
  }
765
691
  });
766
- login = (provider) => {
767
- if (this.alepha.isBrowser()) {
768
- const browser = this.alepha.get(ReactBrowserProvider);
769
- const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
770
- window.location.href = `${this.api}?redirect=${redirect}`;
771
- if (browser.transitioning) {
772
- throw new RedirectionError(browser.state.pathname);
773
- }
774
- return;
775
- }
776
- throw new RedirectionError(this.api);
777
- };
778
- logout = () => {
779
- window.location.href = `/api/_oauth/logout?redirect=${encodeURIComponent(window.location.origin)}`;
780
- };
781
692
  }
782
693
 
783
694
  class RouterHookApi {
@@ -900,14 +811,23 @@ const useRouter = () => {
900
811
  layer,
901
812
  ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
902
813
  ),
903
- [ctx.router, layer]
814
+ [layer]
904
815
  );
905
816
  };
906
817
 
907
818
  const Link = (props) => {
908
819
  React.useContext(RouterContext);
820
+ const to = typeof props.to === "string" ? props.to : props.to.options.path;
821
+ if (!to) {
822
+ return null;
823
+ }
824
+ const can = typeof props.to === "string" ? void 0 : props.to.options.can;
825
+ if (can && !can()) {
826
+ return null;
827
+ }
828
+ const name = typeof props.to === "string" ? void 0 : props.to.options.name;
909
829
  const router = useRouter();
910
- return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(props.to), ...props, children: props.children });
830
+ return /* @__PURE__ */ jsx("a", { ...router.createAnchorProps(to), ...props, children: props.children ?? name });
911
831
  };
912
832
 
913
833
  const useInject = (clazz) => {
@@ -915,7 +835,9 @@ const useInject = (clazz) => {
915
835
  if (!ctx) {
916
836
  throw new Error("useRouter must be used within a <RouterProvider>");
917
837
  }
918
- return ctx.alepha.get(clazz);
838
+ return ctx.alepha.get(clazz, {
839
+ skipRegistration: true
840
+ });
919
841
  };
920
842
 
921
843
  const useClient = () => {
@@ -972,13 +894,13 @@ const useRouterEvents = (opts = {}) => {
972
894
  const onEnd = opts.onEnd;
973
895
  const onError = opts.onError;
974
896
  if (onBegin) {
975
- subs.push(ctx.router.on("begin", onBegin));
897
+ subs.push(ctx.events.on("begin", onBegin));
976
898
  }
977
899
  if (onEnd) {
978
- subs.push(ctx.router.on("end", onEnd));
900
+ subs.push(ctx.events.on("end", onEnd));
979
901
  }
980
902
  if (onError) {
981
- subs.push(ctx.router.on("error", onError));
903
+ subs.push(ctx.events.on("error", onError));
982
904
  }
983
905
  return () => {
984
906
  for (const sub of subs) {
@@ -996,7 +918,7 @@ const useRouterState = () => {
996
918
  }
997
919
  const [state, setState] = useState(ctx.state);
998
920
  useEffect(
999
- () => ctx.router.on("end", (it) => {
921
+ () => ctx.events.on("end", (it) => {
1000
922
  setState({ ...it });
1001
923
  }),
1002
924
  []
@@ -1020,7 +942,7 @@ const useActive = (path) => {
1020
942
  const [isPending, setPending] = useState(false);
1021
943
  const isActive = current === href;
1022
944
  useEffect(
1023
- () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
945
+ () => ctx.events.on("end", ({ pathname }) => setCurrent(pathname)),
1024
946
  []
1025
947
  );
1026
948
  return {
@@ -1043,21 +965,4 @@ const useActive = (path) => {
1043
965
  };
1044
966
  };
1045
967
 
1046
- const useAuth = () => {
1047
- const ctx = useContext(RouterContext);
1048
- if (!ctx) {
1049
- throw new Error("useAuth must be used within a RouterContext");
1050
- }
1051
- const args = ctx.args ?? {};
1052
- return {
1053
- user: args.user,
1054
- logout: () => {
1055
- ctx.alepha.get(Auth).logout();
1056
- },
1057
- login: (provider) => {
1058
- ctx.alepha.get(Auth).login();
1059
- }
1060
- };
1061
- };
1062
-
1063
- 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 };