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