@alepha/react 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -28
  2. package/dist/index.browser.cjs +20 -23
  3. package/dist/index.browser.cjs.map +1 -0
  4. package/dist/index.browser.js +8 -7
  5. package/dist/index.browser.js.map +1 -0
  6. package/dist/index.cjs +234 -542
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.ts +218 -682
  9. package/dist/index.js +219 -522
  10. package/dist/index.js.map +1 -0
  11. package/dist/{useAuth-B9ypF48n.cjs → useActive-BGtt_RNQ.cjs} +310 -475
  12. package/dist/useActive-BGtt_RNQ.cjs.map +1 -0
  13. package/dist/{useAuth-Ps01oe8e.js → useActive-QkvcaSmu.js} +309 -471
  14. package/dist/useActive-QkvcaSmu.js.map +1 -0
  15. package/package.json +12 -10
  16. package/src/components/Link.tsx +35 -0
  17. package/src/components/NestedView.tsx +36 -0
  18. package/src/contexts/RouterContext.ts +18 -0
  19. package/src/contexts/RouterLayerContext.ts +10 -0
  20. package/src/descriptors/$page.ts +143 -0
  21. package/src/errors/RedirectionError.ts +7 -0
  22. package/src/hooks/RouterHookApi.ts +156 -0
  23. package/src/hooks/useActive.ts +57 -0
  24. package/src/hooks/useClient.ts +6 -0
  25. package/src/hooks/useInject.ts +14 -0
  26. package/src/hooks/useQueryParams.ts +59 -0
  27. package/src/hooks/useRouter.ts +25 -0
  28. package/src/hooks/useRouterEvents.ts +43 -0
  29. package/src/hooks/useRouterState.ts +23 -0
  30. package/src/index.browser.ts +21 -0
  31. package/src/index.shared.ts +15 -0
  32. package/src/index.ts +48 -0
  33. package/src/providers/BrowserHeadProvider.ts +43 -0
  34. package/src/providers/BrowserRouterProvider.ts +146 -0
  35. package/src/providers/PageDescriptorProvider.ts +534 -0
  36. package/src/providers/ReactBrowserProvider.ts +223 -0
  37. package/src/providers/ReactServerProvider.ts +278 -0
  38. package/src/providers/ServerHeadProvider.ts +91 -0
@@ -1,43 +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
- return {
14
- [core.KIND]: KEY,
15
- options,
16
- jwks: () => {
17
- return options.oidc?.issuer ?? "";
13
+ if (options.children) {
14
+ for (const child of options.children) {
15
+ child.options.parent = {
16
+ options
17
+ };
18
18
  }
19
- };
20
- };
21
- $auth[core.KIND] = KEY;
22
-
23
- const pageDescriptorKey = "PAGE";
24
- const $page = (options) => {
25
- core.__descriptor(pageDescriptorKey);
19
+ }
20
+ if (options.parent) {
21
+ options.parent.options.children ??= [];
22
+ options.parent.options.children.push({
23
+ options
24
+ });
25
+ }
26
26
  return {
27
- [core.KIND]: pageDescriptorKey,
27
+ [core.KIND]: KEY,
28
28
  options,
29
29
  render: () => {
30
- throw new core.NotImplementedError(pageDescriptorKey);
30
+ throw new core.NotImplementedError(KEY);
31
31
  },
32
32
  go: () => {
33
- throw new core.NotImplementedError(pageDescriptorKey);
33
+ throw new core.NotImplementedError(KEY);
34
34
  },
35
35
  createAnchorProps: () => {
36
- 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;
37
43
  }
38
44
  };
39
45
  };
40
- $page[core.KIND] = pageDescriptorKey;
46
+ $page[core.KIND] = KEY;
41
47
 
42
48
  const RouterContext = React.createContext(
43
49
  void 0
@@ -54,7 +60,7 @@ const NestedView = (props) => {
54
60
  );
55
61
  React.useEffect(() => {
56
62
  if (app?.alepha.isBrowser()) {
57
- return app?.router.on("end", (state) => {
63
+ return app?.events.on("end", (state) => {
58
64
  setView(state.layers[index]?.element);
59
65
  });
60
66
  }
@@ -69,159 +75,37 @@ class RedirectionError extends Error {
69
75
  }
70
76
  }
71
77
 
72
- class Router extends core.EventEmitter {
78
+ class PageDescriptorProvider {
73
79
  log = core.$logger();
74
80
  alepha = core.$inject(core.Alepha);
75
81
  pages = [];
76
- notFoundPageRoute;
77
- /**
78
- * Get the page by name.
79
- *
80
- * @param name - Page name
81
- * @return PageRoute
82
- */
82
+ getPages() {
83
+ return this.pages;
84
+ }
83
85
  page(name) {
84
- const found = this.pages.find((it) => it.name === name);
85
- if (!found) {
86
- throw new Error(`Page ${name} not found`);
86
+ for (const page of this.pages) {
87
+ if (page.name === name) {
88
+ return page;
89
+ }
87
90
  }
88
- return found;
91
+ throw new Error(`Page ${name} not found`);
89
92
  }
90
- /**
91
- *
92
- */
93
- root(state, context = {}) {
93
+ root(state, context = {}, events) {
94
94
  return React.createElement(
95
95
  RouterContext.Provider,
96
96
  {
97
97
  value: {
98
- state,
99
- router: this,
100
98
  alepha: this.alepha,
101
- args: context
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,14 +137,15 @@ 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) {
@@ -275,15 +160,14 @@ class Router extends core.EventEmitter {
275
160
  forceRefresh = true;
276
161
  }
277
162
  try {
278
- const props = await route2.resolve?.(
279
- {
280
- ...config,
281
- ...context,
282
- context: args,
283
- url
284
- },
285
- args ?? {}
286
- ) ?? {};
163
+ const props = await route2.resolve?.({
164
+ ...request,
165
+ // request
166
+ ...config,
167
+ // params, query
168
+ ...context
169
+ // previous props
170
+ }) ?? {};
287
171
  it.props = {
288
172
  ...props
289
173
  };
@@ -293,7 +177,13 @@ class Router extends core.EventEmitter {
293
177
  };
294
178
  } catch (e) {
295
179
  if (e instanceof RedirectionError) {
296
- 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
+ };
297
187
  }
298
188
  this.log.error(e);
299
189
  it.error = e;
@@ -304,25 +194,25 @@ class Router extends core.EventEmitter {
304
194
  for (let i = 0; i < stack.length; i++) {
305
195
  const it = stack[i];
306
196
  const props = it.props ?? {};
307
- const params2 = { ...it.config?.params };
308
- for (const key of Object.keys(params2)) {
309
- 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]);
310
200
  }
311
- if (it.route.head && renderContext && !it.error) {
312
- this.mergeRenderContext(it.route, renderContext, {
201
+ if (it.route.head && !it.error) {
202
+ this.fillHead(it.route, request, {
313
203
  ...props,
314
204
  ...context
315
205
  });
316
206
  }
317
207
  acc += "/";
318
- acc += it.route.path ? pathToRegexp.compile(it.route.path)(params2) : "";
208
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
319
209
  const path = acc.replace(/\/+/, "/");
320
210
  if (it.error) {
321
211
  const errorHandler = this.getErrorHandler(it.route);
322
212
  const element = await (errorHandler ? errorHandler({
323
213
  ...it.config,
324
214
  error: it.error,
325
- url
215
+ url: ""
326
216
  }) : this.renderError(it.error));
327
217
  layers.push({
328
218
  props,
@@ -350,13 +240,8 @@ class Router extends core.EventEmitter {
350
240
  path
351
241
  });
352
242
  }
353
- return layers;
243
+ return { layers, head: request.head, pathname, search };
354
244
  }
355
- /**
356
- *
357
- * @param route
358
- * @protected
359
- */
360
245
  getErrorHandler(route) {
361
246
  if (route.errorHandler) return route.errorHandler;
362
247
  let parent = route.parent;
@@ -365,12 +250,6 @@ class Router extends core.EventEmitter {
365
250
  parent = parent.parent;
366
251
  }
367
252
  }
368
- /**
369
- *
370
- * @param page
371
- * @param props
372
- * @protected
373
- */
374
253
  async createElement(page, props) {
375
254
  if (page.lazy) {
376
255
  const component = await page.lazy();
@@ -381,65 +260,43 @@ class Router extends core.EventEmitter {
381
260
  }
382
261
  return void 0;
383
262
  }
384
- /**
385
- * Merge the render context with the page context.
386
- *
387
- * @param page
388
- * @param ctx
389
- * @param props
390
- * @protected
391
- */
392
- mergeRenderContext(page, ctx, props) {
393
- if (page.head) {
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) {
394
270
  ctx.head ??= {};
395
- const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
396
- if (head.title) {
397
- ctx.head ??= {};
398
- if (ctx.head.titleSeparator) {
399
- ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
400
- } else {
401
- ctx.head.title = head.title;
402
- }
403
- ctx.head.titleSeparator = head.titleSeparator;
404
- }
405
- if (head.htmlAttributes) {
406
- ctx.head.htmlAttributes = {
407
- ...ctx.head.htmlAttributes,
408
- ...head.htmlAttributes
409
- };
410
- }
411
- if (head.bodyAttributes) {
412
- ctx.head.bodyAttributes = {
413
- ...ctx.head.bodyAttributes,
414
- ...head.bodyAttributes
415
- };
416
- }
417
- if (head.meta) {
418
- ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
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;
419
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 ?? []];
420
292
  }
421
293
  }
422
- /**
423
- *
424
- * @param e
425
- * @protected
426
- */
427
294
  renderError(e) {
428
295
  return React.createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
429
296
  }
430
- /**
431
- * Render an empty view.
432
- *
433
- * @protected
434
- */
435
297
  renderEmptyView() {
436
298
  return React.createElement(NestedView, {});
437
299
  }
438
- /**
439
- * Create a valid href for the given page.
440
- * @param page
441
- * @param params
442
- */
443
300
  href(page, params = {}) {
444
301
  const found = this.pages.find((it) => it.name === page.options.name);
445
302
  if (!found) {
@@ -451,16 +308,15 @@ class Router extends core.EventEmitter {
451
308
  url = `${parent.path ?? ""}/${url}`;
452
309
  parent = parent.parent;
453
310
  }
454
- url = pathToRegexp.compile(url)(params);
311
+ url = this.compile(url, params);
455
312
  return url.replace(/\/\/+/g, "/") || "/";
456
313
  }
457
- /**
458
- *
459
- * @param index
460
- * @param path
461
- * @param view
462
- * @protected
463
- */
314
+ compile(path, params = {}) {
315
+ for (const [key, value] of Object.entries(params)) {
316
+ path = path.replace(`:${key}`, value);
317
+ }
318
+ return path;
319
+ }
464
320
  renderView(index, path, view = this.renderEmptyView()) {
465
321
  return React.createElement(
466
322
  RouterLayerContext.Provider,
@@ -473,23 +329,39 @@ class Router extends core.EventEmitter {
473
329
  view
474
330
  );
475
331
  }
476
- /**
477
- *
478
- * @param entry
479
- */
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
+ }
480
358
  add(entry) {
481
359
  if (this.alepha.isReady()) {
482
360
  throw new Error("Router is already initialized");
483
361
  }
484
- if (entry.notFoundHandler) {
485
- this.notFoundPageRoute = {
486
- name: "not-found",
487
- component: entry.notFoundHandler
488
- };
489
- }
490
362
  entry.name ??= this.nextId();
491
363
  const page = entry;
492
- page.match = this.createMatchFunction(page);
364
+ page.match = this.createMatch(page);
493
365
  this.pages.push(page);
494
366
  if (page.children) {
495
367
  for (const child of page.children) {
@@ -498,13 +370,7 @@ class Router extends core.EventEmitter {
498
370
  }
499
371
  }
500
372
  }
501
- /**
502
- * Create a match function for the given page.
503
- *
504
- * @param page
505
- * @protected
506
- */
507
- createMatchFunction(page) {
373
+ createMatch(page) {
508
374
  let url = page.path ?? "/";
509
375
  let target = page.parent;
510
376
  while (target) {
@@ -512,76 +378,166 @@ class Router extends core.EventEmitter {
512
378
  target = target.parent;
513
379
  }
514
380
  let path = url.replace(/\/\/+/g, "/");
515
- if (path.endsWith("/")) {
381
+ if (path.endsWith("/") && path !== "/") {
516
382
  path = path.slice(0, -1);
517
383
  }
518
- if (path.includes("?")) {
519
- return {
520
- exec: pathToRegexp.match(path.split("?")[0]),
521
- path
522
- };
523
- }
524
- return {
525
- exec: pathToRegexp.match(path),
526
- path
527
- };
384
+ return path;
528
385
  }
529
- /**
530
- *
531
- */
532
- empty() {
533
- return this.pages.length === 0;
534
- }
535
- /**
536
- *
537
- * @protected
538
- */
539
386
  _next = 0;
540
- /**
541
- *
542
- * @protected
543
- */
544
387
  nextId() {
545
388
  this._next += 1;
546
389
  return `P${this._next}`;
547
390
  }
548
391
  }
392
+ const isPageRoute = (it) => {
393
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
394
+ };
549
395
 
550
- 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();
551
437
  alepha = core.$inject(core.Alepha);
552
- router = core.$inject(Router);
438
+ pageDescriptorProvider = core.$inject(PageDescriptorProvider);
439
+ events = new core.EventEmitter();
440
+ add(entry) {
441
+ this.pageDescriptorProvider.add(entry);
442
+ }
553
443
  configure = core.$hook({
554
444
  name: "configure",
555
- handler: () => {
556
- const pages = this.alepha.getDescriptorValues($page);
557
- for (const { value, key } of pages) {
558
- value.options.name ??= key;
559
- if (pages.find((it) => it.value.options.children?.().includes(value))) {
560
- 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
+ });
561
452
  }
562
- this.router.add(this.map(pages, value));
563
453
  }
564
454
  }
565
455
  });
566
- /**
567
- * Transform
568
- * @param pages
569
- * @param target
570
- * @protected
571
- */
572
- map(pages, target) {
573
- const children = target.options.children?.() ?? [];
574
- for (const it of pages) {
575
- if (it.value.options.parent === target) {
576
- 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;
577
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);
578
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);
579
532
  return {
580
- ...target.options,
581
- parent: void 0,
582
- children: children.map((it) => this.map(pages, it))
533
+ element: this.root(state, options.context),
534
+ layers: options.state.layers,
535
+ head: state.head
583
536
  };
584
537
  }
538
+ root(state, context = {}) {
539
+ return this.pageDescriptorProvider.root(state, context, this.events);
540
+ }
585
541
  }
586
542
 
587
543
  const envSchema = core.t.object({
@@ -590,7 +546,9 @@ const envSchema = core.t.object({
590
546
  class ReactBrowserProvider {
591
547
  log = core.$logger();
592
548
  client = core.$inject(server.HttpClient);
593
- router = core.$inject(Router);
549
+ alepha = core.$inject(core.Alepha);
550
+ router = core.$inject(BrowserRouterProvider);
551
+ headProvider = core.$inject(BrowserHeadProvider);
594
552
  env = core.$inject(envSchema);
595
553
  root;
596
554
  transitioning;
@@ -598,30 +556,17 @@ class ReactBrowserProvider {
598
556
  layers: [],
599
557
  pathname: "",
600
558
  search: "",
601
- context: {}
559
+ head: {}
602
560
  };
603
- /**
604
- *
605
- */
606
561
  get document() {
607
562
  return window.document;
608
563
  }
609
- /**
610
- *
611
- */
612
564
  get history() {
613
565
  return window.history;
614
566
  }
615
- /**
616
- *
617
- */
618
567
  get url() {
619
568
  return window.location.pathname + window.location.search;
620
569
  }
621
- /**
622
- *
623
- * @param props
624
- */
625
570
  async invalidate(props) {
626
571
  const previous = [];
627
572
  if (props) {
@@ -662,66 +607,22 @@ class ReactBrowserProvider {
662
607
  }
663
608
  this.history.pushState({}, "", url);
664
609
  }
665
- /**
666
- *
667
- * @param options
668
- * @protected
669
- */
670
610
  async render(options = {}) {
671
611
  const previous = options.previous ?? this.state.layers;
672
612
  const url = options.url ?? this.url;
673
613
  this.transitioning = { to: url };
674
- const result = await this.router.render(url, {
675
- previous,
676
- state: this.state
677
- });
614
+ const result = await this.router.transition(
615
+ new URL(`http://localhost${url}`),
616
+ {
617
+ previous,
618
+ state: this.state
619
+ }
620
+ );
678
621
  if (result.redirect) {
679
622
  return await this.render({ url: result.redirect });
680
623
  }
681
624
  this.transitioning = void 0;
682
- return { url, context: result.context };
683
- }
684
- /**
685
- * Render the helmet context.
686
- *
687
- * @param ctx
688
- * @protected
689
- */
690
- renderHeadContext(ctx) {
691
- if (ctx.title) {
692
- this.document.title = ctx.title;
693
- }
694
- if (ctx.bodyAttributes) {
695
- for (const [key, value] of Object.entries(ctx.bodyAttributes)) {
696
- if (value) {
697
- this.document.body.setAttribute(key, value);
698
- } else {
699
- this.document.body.removeAttribute(key);
700
- }
701
- }
702
- }
703
- if (ctx.htmlAttributes) {
704
- for (const [key, value] of Object.entries(ctx.htmlAttributes)) {
705
- if (value) {
706
- this.document.documentElement.setAttribute(key, value);
707
- } else {
708
- this.document.documentElement.removeAttribute(key);
709
- }
710
- }
711
- }
712
- if (ctx.meta) {
713
- for (const [key, value] of Object.entries(ctx.meta)) {
714
- const meta = this.document.querySelector(`meta[name="${key}"]`);
715
- if (meta) {
716
- meta.setAttribute("content", value.content);
717
- } else {
718
- const newMeta = this.document.createElement("meta");
719
- newMeta.setAttribute("name", key);
720
- newMeta.setAttribute("content", value.content);
721
- this.document.head.appendChild(newMeta);
722
- }
723
- }
724
- }
625
+ return { url, head: result.head };
725
626
  }
726
627
  /**
727
628
  * Get embedded layers from the server.
@@ -751,18 +652,6 @@ class ReactBrowserProvider {
751
652
  this.document.body.prepend(div);
752
653
  return div;
753
654
  }
754
- getUserFromCookies() {
755
- const cookies = this.document.cookie.split("; ");
756
- const userCookie = cookies.find((cookie) => cookie.startsWith("user="));
757
- try {
758
- if (userCookie) {
759
- return JSON.parse(decodeURIComponent(userCookie.split("=")[1]));
760
- }
761
- } catch (error) {
762
- this.log.warn(error, "Failed to parse user cookie");
763
- }
764
- return void 0;
765
- }
766
655
  // -------------------------------------------------------------------------------------------------------------------
767
656
  /**
768
657
  *
@@ -771,18 +660,21 @@ class ReactBrowserProvider {
771
660
  ready = core.$hook({
772
661
  name: "ready",
773
662
  handler: async () => {
774
- const cache = this.getHydrationState();
775
- const previous = cache?.layers ?? [];
776
- if (cache?.links) {
777
- this.client.links = cache.links;
778
- }
779
- const { context } = await this.render({ previous });
780
- if (context.head) {
781
- this.renderHeadContext(context.head);
782
- }
783
- const element = this.router.root(this.state, {
784
- user: cache?.user ?? this.getUserFromCookies()
663
+ const hydration = this.getHydrationState();
664
+ const previous = hydration?.layers ?? [];
665
+ if (hydration?.links) {
666
+ this.client.links = hydration.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.emit("react:browser:render", {
674
+ context,
675
+ hydration
785
676
  });
677
+ const element = this.router.root(this.state, context);
786
678
  if (previous.length > 0) {
787
679
  this.root = client.hydrateRoot(this.getRootElement(), element);
788
680
  this.log.info("Hydrated root element");
@@ -794,48 +686,11 @@ class ReactBrowserProvider {
794
686
  window.addEventListener("popstate", () => {
795
687
  this.render();
796
688
  });
797
- this.router.on("end", ({ context: context2 }) => {
798
- if (context2.head) {
799
- this.renderHeadContext(context2.head);
800
- }
801
- });
802
- }
803
- });
804
- }
805
-
806
- class Auth {
807
- alepha = core.$inject(core.Alepha);
808
- log = core.$logger();
809
- client = core.$inject(server.HttpClient);
810
- slugs = {
811
- login: "/api/_oauth/login",
812
- logout: "/api/_oauth/logout"
813
- };
814
- start = core.$hook({
815
- name: "start",
816
- handler: async () => {
817
- this.client.on("onError", (err) => {
818
- if (err.statusCode === 401) {
819
- this.login();
820
- }
689
+ this.router.events.on("end", ({ head: head2 }) => {
690
+ this.headProvider.renderHead(this.document, head2);
821
691
  });
822
692
  }
823
693
  });
824
- login = (provider) => {
825
- if (this.alepha.isBrowser()) {
826
- const browser = this.alepha.get(ReactBrowserProvider);
827
- const redirect = browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href;
828
- window.location.href = `${this.slugs.login}?redirect=${redirect}`;
829
- if (browser.transitioning) {
830
- throw new RedirectionError(browser.state.pathname);
831
- }
832
- return;
833
- }
834
- throw new RedirectionError(this.slugs.login);
835
- };
836
- logout = () => {
837
- window.location.href = `${this.slugs.logout}?redirect=${encodeURIComponent(window.location.origin)}`;
838
- };
839
694
  }
840
695
 
841
696
  class RouterHookApi {
@@ -958,7 +813,7 @@ const useRouter = () => {
958
813
  layer,
959
814
  ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
960
815
  ),
961
- [ctx.router, layer]
816
+ [layer]
962
817
  );
963
818
  };
964
819
 
@@ -970,7 +825,6 @@ const Link = (props) => {
970
825
  }
971
826
  const can = typeof props.to === "string" ? void 0 : props.to.options.can;
972
827
  if (can && !can()) {
973
- console.log("I cannot go to", to);
974
828
  return null;
975
829
  }
976
830
  const name = typeof props.to === "string" ? void 0 : props.to.options.name;
@@ -1042,13 +896,13 @@ const useRouterEvents = (opts = {}) => {
1042
896
  const onEnd = opts.onEnd;
1043
897
  const onError = opts.onError;
1044
898
  if (onBegin) {
1045
- subs.push(ctx.router.on("begin", onBegin));
899
+ subs.push(ctx.events.on("begin", onBegin));
1046
900
  }
1047
901
  if (onEnd) {
1048
- subs.push(ctx.router.on("end", onEnd));
902
+ subs.push(ctx.events.on("end", onEnd));
1049
903
  }
1050
904
  if (onError) {
1051
- subs.push(ctx.router.on("error", onError));
905
+ subs.push(ctx.events.on("error", onError));
1052
906
  }
1053
907
  return () => {
1054
908
  for (const sub of subs) {
@@ -1066,7 +920,7 @@ const useRouterState = () => {
1066
920
  }
1067
921
  const [state, setState] = React.useState(ctx.state);
1068
922
  React.useEffect(
1069
- () => ctx.router.on("end", (it) => {
923
+ () => ctx.events.on("end", (it) => {
1070
924
  setState({ ...it });
1071
925
  }),
1072
926
  []
@@ -1090,7 +944,7 @@ const useActive = (path) => {
1090
944
  const [isPending, setPending] = React.useState(false);
1091
945
  const isActive = current === href;
1092
946
  React.useEffect(
1093
- () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
947
+ () => ctx.events.on("end", ({ pathname }) => setCurrent(pathname)),
1094
948
  []
1095
949
  );
1096
950
  return {
@@ -1113,41 +967,22 @@ const useActive = (path) => {
1113
967
  };
1114
968
  };
1115
969
 
1116
- const useAuth = () => {
1117
- const ctx = React.useContext(RouterContext);
1118
- if (!ctx) {
1119
- throw new Error("useAuth must be used within a RouterContext");
1120
- }
1121
- const args = ctx.args ?? {};
1122
- return {
1123
- user: args.user,
1124
- logout: () => {
1125
- ctx.alepha.get(Auth).logout();
1126
- },
1127
- login: (provider) => {
1128
- ctx.alepha.get(Auth).login();
1129
- }
1130
- };
1131
- };
1132
-
1133
- exports.$auth = $auth;
1134
970
  exports.$page = $page;
1135
- exports.Auth = Auth;
971
+ exports.BrowserRouterProvider = BrowserRouterProvider;
1136
972
  exports.Link = Link;
1137
973
  exports.NestedView = NestedView;
1138
974
  exports.PageDescriptorProvider = PageDescriptorProvider;
1139
975
  exports.ReactBrowserProvider = ReactBrowserProvider;
1140
976
  exports.RedirectionError = RedirectionError;
1141
- exports.Router = Router;
1142
977
  exports.RouterContext = RouterContext;
1143
978
  exports.RouterHookApi = RouterHookApi;
1144
979
  exports.RouterLayerContext = RouterLayerContext;
1145
- exports.pageDescriptorKey = pageDescriptorKey;
980
+ exports.isPageRoute = isPageRoute;
1146
981
  exports.useActive = useActive;
1147
- exports.useAuth = useAuth;
1148
982
  exports.useClient = useClient;
1149
983
  exports.useInject = useInject;
1150
984
  exports.useQueryParams = useQueryParams;
1151
985
  exports.useRouter = useRouter;
1152
986
  exports.useRouterEvents = useRouterEvents;
1153
987
  exports.useRouterState = useRouterState;
988
+ //# sourceMappingURL=useActive-BGtt_RNQ.cjs.map