@alepha/react 0.13.2 → 0.13.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.
@@ -4,16 +4,17 @@ import { useCallback, useEffect, useMemo } from "react";
4
4
 
5
5
  //#region src/head/providers/HeadProvider.ts
6
6
  var HeadProvider = class {
7
- global;
8
- getGlobalHead() {
9
- if (typeof this.global === "function") return this.global();
10
- return this.global;
11
- }
7
+ global = [];
12
8
  fillHead(state) {
13
- state.head = {
14
- ...state.head,
15
- ...this.getGlobalHead()
16
- };
9
+ state.head = { ...state.head };
10
+ for (const h of this.global ?? []) {
11
+ const head = typeof h === "function" ? h() : h;
12
+ state.head = {
13
+ ...state.head,
14
+ ...head,
15
+ meta: [...state.head.meta ?? [], ...head.meta ?? []]
16
+ };
17
+ }
17
18
  for (const layer of state.layers) if (layer.route?.head && !layer.error) this.fillHeadByPage(layer.route, state, layer.props ?? {});
18
19
  }
19
20
  fillHeadByPage(page, state, props) {
@@ -49,7 +50,7 @@ const $head = (options) => {
49
50
  var HeadPrimitive = class extends Primitive {
50
51
  provider = $inject(HeadProvider);
51
52
  onInit() {
52
- this.provider.global = this.options;
53
+ this.provider.global = [...this.provider.global ?? [], this.options];
53
54
  }
54
55
  };
55
56
  $head[KIND] = HeadPrimitive;
@@ -121,6 +122,16 @@ var BrowserHeadProvider = class {
121
122
  document.head.appendChild(newMeta);
122
123
  }
123
124
  }
125
+ if (head.link) for (const it of head.link) {
126
+ const { rel, href } = it;
127
+ let link = document.querySelector(`link[rel="${rel}"][href="${href}"]`);
128
+ if (!link) {
129
+ link = document.createElement("link");
130
+ link.setAttribute("rel", rel);
131
+ link.setAttribute("href", href);
132
+ document.head.appendChild(link);
133
+ }
134
+ }
124
135
  }
125
136
  };
126
137
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","names":["attrs: Record<string, string>","metas: { name: string; content: string }[]"],"sources":["../../src/head/providers/HeadProvider.ts","../../src/head/primitives/$head.ts","../../src/head/providers/BrowserHeadProvider.ts","../../src/head/hooks/useHead.ts","../../src/head/index.browser.ts"],"sourcesContent":["import type { PageRoute, ReactRouterState } from \"@alepha/react\";\nimport type { Head } from \"../interfaces/Head.ts\";\n\nexport class HeadProvider {\n public global?: Head | (() => Head);\n\n protected getGlobalHead(): Head | undefined {\n if (typeof this.global === \"function\") {\n return this.global();\n }\n return this.global;\n }\n\n public fillHead(state: ReactRouterState) {\n state.head = {\n ...state.head,\n ...this.getGlobalHead(),\n };\n\n for (const layer of state.layers) {\n if (layer.route?.head && !layer.error) {\n this.fillHeadByPage(layer.route, state, layer.props ?? {});\n }\n }\n }\n\n protected fillHeadByPage(\n page: PageRoute,\n state: ReactRouterState,\n props: Record<string, any>,\n ): void {\n if (!page.head) {\n return;\n }\n\n state.head ??= {};\n\n const head =\n typeof page.head === \"function\"\n ? page.head(props, state.head)\n : page.head;\n\n if (head.title) {\n state.head ??= {};\n\n if (state.head.titleSeparator) {\n state.head.title = `${head.title}${state.head.titleSeparator}${state.head.title}`;\n } else {\n state.head.title = head.title;\n }\n\n state.head.titleSeparator = head.titleSeparator;\n }\n\n if (head.htmlAttributes) {\n state.head.htmlAttributes = {\n ...state.head.htmlAttributes,\n ...head.htmlAttributes,\n };\n }\n\n if (head.bodyAttributes) {\n state.head.bodyAttributes = {\n ...state.head.bodyAttributes,\n ...head.bodyAttributes,\n };\n }\n\n if (head.meta) {\n state.head.meta = [...(state.head.meta ?? []), ...(head.meta ?? [])];\n }\n }\n}\n","import { $inject, createPrimitive, Primitive, KIND } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"../providers/HeadProvider.ts\";\n\n/**\n * Set global `<head>` options for the application.\n */\nexport const $head = (options: HeadPrimitiveOptions) => {\n return createPrimitive(HeadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type HeadPrimitiveOptions = Head | (() => Head);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class HeadPrimitive extends Primitive<HeadPrimitiveOptions> {\n protected readonly provider = $inject(HeadProvider);\n protected onInit() {\n this.provider.global = this.options;\n }\n}\n\n$head[KIND] = HeadPrimitive;\n","import { $hook, $inject } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class BrowserHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n\n protected get document(): Document {\n return window.document;\n }\n\n protected readonly onBrowserRender = $hook({\n on: \"react:browser:render\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n protected readonly onTransitionEnd = $hook({\n on: \"react:transition:end\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n public getHead(document: Document): Head {\n return {\n get title() {\n return document.title;\n },\n get htmlAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.documentElement.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get bodyAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.body.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get meta() {\n const metas: { name: string; content: string }[] = [];\n for (const meta of document.head.querySelectorAll(\"meta[name]\")) {\n const name = meta.getAttribute(\"name\");\n const content = meta.getAttribute(\"content\");\n if (name && content) {\n metas.push({ name, content });\n }\n }\n return metas;\n },\n };\n }\n\n public renderHead(document: Document, head: Head): void {\n if (head.title) {\n document.title = head.title;\n }\n\n if (head.bodyAttributes) {\n for (const [key, value] of Object.entries(head.bodyAttributes)) {\n if (value) {\n document.body.setAttribute(key, value);\n } else {\n document.body.removeAttribute(key);\n }\n }\n }\n\n if (head.htmlAttributes) {\n for (const [key, value] of Object.entries(head.htmlAttributes)) {\n if (value) {\n document.documentElement.setAttribute(key, value);\n } else {\n document.documentElement.removeAttribute(key);\n }\n }\n }\n\n if (head.meta) {\n for (const it of head.meta) {\n const { name, content } = it;\n const meta = document.querySelector(`meta[name=\"${name}\"]`);\n if (meta) {\n meta.setAttribute(\"content\", content);\n } else {\n const newMeta = document.createElement(\"meta\");\n newMeta.setAttribute(\"name\", name);\n newMeta.setAttribute(\"content\", content);\n document.head.appendChild(newMeta);\n }\n }\n }\n }\n}\n","import { useInject } from \"@alepha/react\";\nimport { Alepha } from \"alepha\";\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { BrowserHeadProvider } from \"../providers/BrowserHeadProvider.ts\";\n\n/**\n * ```tsx\n * const App = () => {\n * const [head, setHead] = useHead({\n * // will set the document title on the first render\n * title: \"My App\",\n * });\n *\n * return (\n * // This will update the document title when the button is clicked\n * <button onClick={() => setHead({ title: \"Change Title\" })}>\n * Change Title {head.title}\n * </button>\n * );\n * }\n * ```\n */\nexport const useHead = (options?: UseHeadOptions): UseHeadReturn => {\n const alepha = useInject(Alepha);\n\n const current = useMemo(() => {\n if (!alepha.isBrowser()) {\n return {};\n }\n\n return alepha.inject(BrowserHeadProvider).getHead(window.document);\n }, []);\n\n const setHead = useCallback((head?: Head | ((previous?: Head) => Head)) => {\n if (!alepha.isBrowser()) {\n return;\n }\n\n alepha\n .inject(BrowserHeadProvider)\n .renderHead(\n window.document,\n typeof head === \"function\" ? head(current) : head || {},\n );\n }, []);\n\n useEffect(() => {\n if (options) {\n setHead(options);\n }\n }, []);\n\n return [current, setHead];\n};\n\nexport type UseHeadOptions = Head | ((previous?: Head) => Head);\n\nexport type UseHeadReturn = [\n Head,\n (head?: Head | ((previous?: Head) => Head)) => void,\n];\n","import { AlephaReact } from \"@alepha/react\";\nimport { $module } from \"alepha\";\nimport { $head } from \"./primitives/$head.ts\";\nimport { BrowserHeadProvider } from \"./providers/BrowserHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$head.ts\";\nexport * from \"./hooks/useHead.ts\";\nexport * from \"./interfaces/Head.ts\";\nexport * from \"./providers/BrowserHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Alepha React Head Module\n *\n * @see {@link BrowserHeadProvider}\n * @module alepha.react.head\n */\nexport const AlephaReactHead = $module({\n name: \"alepha.react.head\",\n primitives: [$head],\n services: [AlephaReact, BrowserHeadProvider],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.browser.js","names":["attrs: Record<string, string>","metas: { name: string; content: string }[]"],"sources":["../../src/head/providers/HeadProvider.ts","../../src/head/primitives/$head.ts","../../src/head/providers/BrowserHeadProvider.ts","../../src/head/hooks/useHead.ts","../../src/head/index.browser.ts"],"sourcesContent":["import type { PageRoute, ReactRouterState } from \"@alepha/react\";\nimport type { Head } from \"../interfaces/Head.ts\";\n\nexport class HeadProvider {\n public global?: Array<Head | (() => Head)> = [];\n\n public fillHead(state: ReactRouterState) {\n state.head = {\n ...state.head,\n };\n\n for (const h of this.global ?? []) {\n const head =\n typeof h === \"function\" ? h() : h;\n state.head = {\n ...state.head,\n ...head,\n meta: [...(state.head.meta ?? []), ...(head.meta ?? [])],\n };\n }\n\n for (const layer of state.layers) {\n if (layer.route?.head && !layer.error) {\n this.fillHeadByPage(layer.route, state, layer.props ?? {});\n }\n }\n }\n\n protected fillHeadByPage(\n page: PageRoute,\n state: ReactRouterState,\n props: Record<string, any>,\n ): void {\n if (!page.head) {\n return;\n }\n\n state.head ??= {};\n\n const head =\n typeof page.head === \"function\"\n ? page.head(props, state.head)\n : page.head;\n\n if (head.title) {\n state.head ??= {};\n\n if (state.head.titleSeparator) {\n state.head.title = `${head.title}${state.head.titleSeparator}${state.head.title}`;\n } else {\n state.head.title = head.title;\n }\n\n state.head.titleSeparator = head.titleSeparator;\n }\n\n if (head.htmlAttributes) {\n state.head.htmlAttributes = {\n ...state.head.htmlAttributes,\n ...head.htmlAttributes,\n };\n }\n\n if (head.bodyAttributes) {\n state.head.bodyAttributes = {\n ...state.head.bodyAttributes,\n ...head.bodyAttributes,\n };\n }\n\n if (head.meta) {\n state.head.meta = [...(state.head.meta ?? []), ...(head.meta ?? [])];\n }\n }\n}\n","import { $inject, createPrimitive, Primitive, KIND } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"../providers/HeadProvider.ts\";\n\n/**\n * Set global `<head>` options for the application.\n */\nexport const $head = (options: HeadPrimitiveOptions) => {\n return createPrimitive(HeadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type HeadPrimitiveOptions = Head | (() => Head);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class HeadPrimitive extends Primitive<HeadPrimitiveOptions> {\n protected readonly provider = $inject(HeadProvider);\n protected onInit() {\n this.provider.global = [\n ...(this.provider.global ?? []),\n this.options,\n ];\n }\n}\n\n$head[KIND] = HeadPrimitive;\n","import { $hook, $inject } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class BrowserHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n\n protected get document(): Document {\n return window.document;\n }\n\n protected readonly onBrowserRender = $hook({\n on: \"react:browser:render\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n protected readonly onTransitionEnd = $hook({\n on: \"react:transition:end\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n public getHead(document: Document): Head {\n return {\n get title() {\n return document.title;\n },\n get htmlAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.documentElement.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get bodyAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.body.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get meta() {\n const metas: { name: string; content: string }[] = [];\n for (const meta of document.head.querySelectorAll(\"meta[name]\")) {\n const name = meta.getAttribute(\"name\");\n const content = meta.getAttribute(\"content\");\n if (name && content) {\n metas.push({ name, content });\n }\n }\n return metas;\n },\n };\n }\n\n public renderHead(document: Document, head: Head): void {\n if (head.title) {\n document.title = head.title;\n }\n\n if (head.bodyAttributes) {\n for (const [key, value] of Object.entries(head.bodyAttributes)) {\n if (value) {\n document.body.setAttribute(key, value);\n } else {\n document.body.removeAttribute(key);\n }\n }\n }\n\n if (head.htmlAttributes) {\n for (const [key, value] of Object.entries(head.htmlAttributes)) {\n if (value) {\n document.documentElement.setAttribute(key, value);\n } else {\n document.documentElement.removeAttribute(key);\n }\n }\n }\n\n if (head.meta) {\n for (const it of head.meta) {\n const { name, content } = it;\n const meta = document.querySelector(`meta[name=\"${name}\"]`);\n if (meta) {\n meta.setAttribute(\"content\", content);\n } else {\n const newMeta = document.createElement(\"meta\");\n newMeta.setAttribute(\"name\", name);\n newMeta.setAttribute(\"content\", content);\n document.head.appendChild(newMeta);\n }\n }\n }\n\n if (head.link) {\n for (const it of head.link) {\n const { rel, href } = it;\n let link = document.querySelector(`link[rel=\"${rel}\"][href=\"${href}\"]`);\n if (!link) {\n link = document.createElement(\"link\");\n link.setAttribute(\"rel\", rel);\n link.setAttribute(\"href\", href);\n document.head.appendChild(link);\n }\n }\n }\n }\n}\n","import { useInject } from \"@alepha/react\";\nimport { Alepha } from \"alepha\";\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { BrowserHeadProvider } from \"../providers/BrowserHeadProvider.ts\";\n\n/**\n * ```tsx\n * const App = () => {\n * const [head, setHead] = useHead({\n * // will set the document title on the first render\n * title: \"My App\",\n * });\n *\n * return (\n * // This will update the document title when the button is clicked\n * <button onClick={() => setHead({ title: \"Change Title\" })}>\n * Change Title {head.title}\n * </button>\n * );\n * }\n * ```\n */\nexport const useHead = (options?: UseHeadOptions): UseHeadReturn => {\n const alepha = useInject(Alepha);\n\n const current = useMemo(() => {\n if (!alepha.isBrowser()) {\n return {};\n }\n\n return alepha.inject(BrowserHeadProvider).getHead(window.document);\n }, []);\n\n const setHead = useCallback((head?: Head | ((previous?: Head) => Head)) => {\n if (!alepha.isBrowser()) {\n return;\n }\n\n alepha\n .inject(BrowserHeadProvider)\n .renderHead(\n window.document,\n typeof head === \"function\" ? head(current) : head || {},\n );\n }, []);\n\n useEffect(() => {\n if (options) {\n setHead(options);\n }\n }, []);\n\n return [current, setHead];\n};\n\nexport type UseHeadOptions = Head | ((previous?: Head) => Head);\n\nexport type UseHeadReturn = [\n Head,\n (head?: Head | ((previous?: Head) => Head)) => void,\n];\n","import { AlephaReact } from \"@alepha/react\";\nimport { $module } from \"alepha\";\nimport { $head } from \"./primitives/$head.ts\";\nimport { BrowserHeadProvider } from \"./providers/BrowserHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$head.ts\";\nexport * from \"./hooks/useHead.ts\";\nexport * from \"./interfaces/Head.ts\";\nexport * from \"./providers/BrowserHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Alepha React Head Module\n *\n * @see {@link BrowserHeadProvider}\n * @module alepha.react.head\n */\nexport const AlephaReactHead = $module({\n name: \"alepha.react.head\",\n primitives: [$head],\n services: [AlephaReact, BrowserHeadProvider],\n});\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { PageConfigSchema, PageRoute, ReactRouterState, TPropsDefault, TPropsParentDefault } from "@alepha/react";
2
- import * as alepha30 from "alepha";
2
+ import * as alepha22 from "alepha";
3
3
  import { Alepha, AlephaError, Async, FileLike, InstantiableClass, KIND, LogLevel, LoggerInterface, Primitive, Static, StreamLike, TArray, TFile, TObject, TRecord, TSchema, TStream, TString, TVoid } from "alepha";
4
4
  import { IncomingMessage, Server, ServerResponse } from "node:http";
5
5
  import { Readable } from "node:stream";
@@ -46,12 +46,15 @@ interface SimpleHead {
46
46
  name: string;
47
47
  content: string;
48
48
  }>;
49
+ link?: Array<{
50
+ rel: string;
51
+ href: string;
52
+ }>;
49
53
  }
50
54
  //#endregion
51
55
  //#region src/head/providers/HeadProvider.d.ts
52
56
  declare class HeadProvider {
53
- global?: Head | (() => Head);
54
- protected getGlobalHead(): Head | undefined;
57
+ global?: Array<Head | (() => Head)>;
55
58
  fillHead(state: ReactRouterState): void;
56
59
  protected fillHeadByPage(page: PageRoute, state: ReactRouterState, props: Record<string, any>): void;
57
60
  }
@@ -93,15 +96,15 @@ type UseHeadOptions = Head | ((previous?: Head) => Head);
93
96
  type UseHeadReturn = [Head, (head?: Head | ((previous?: Head) => Head)) => void];
94
97
  //#endregion
95
98
  //#region ../alepha/src/server/schemas/errorSchema.d.ts
96
- declare const errorSchema: alepha30.TObject<{
97
- error: alepha30.TString;
98
- status: alepha30.TInteger;
99
- message: alepha30.TString;
100
- details: alepha30.TOptional<alepha30.TString>;
101
- requestId: alepha30.TOptional<alepha30.TString>;
102
- cause: alepha30.TOptional<alepha30.TObject<{
103
- name: alepha30.TString;
104
- message: alepha30.TString;
99
+ declare const errorSchema: alepha22.TObject<{
100
+ error: alepha22.TString;
101
+ status: alepha22.TInteger;
102
+ message: alepha22.TString;
103
+ details: alepha22.TOptional<alepha22.TString>;
104
+ requestId: alepha22.TOptional<alepha22.TString>;
105
+ cause: alepha22.TOptional<alepha22.TObject<{
106
+ name: alepha22.TString;
107
+ message: alepha22.TString;
105
108
  }>>;
106
109
  }>;
107
110
  type ErrorSchema = Static<typeof errorSchema>;
@@ -340,15 +343,15 @@ interface WebRequestEvent {
340
343
  }
341
344
  //#endregion
342
345
  //#region ../alepha/src/logger/schemas/logEntrySchema.d.ts
343
- declare const logEntrySchema: alepha30.TObject<{
344
- level: alepha30.TUnsafe<"TRACE" | "SILENT" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
345
- message: alepha30.TString;
346
- service: alepha30.TString;
347
- module: alepha30.TString;
348
- context: alepha30.TOptional<alepha30.TString>;
349
- app: alepha30.TOptional<alepha30.TString>;
350
- data: alepha30.TOptional<alepha30.TAny>;
351
- timestamp: alepha30.TNumber;
346
+ declare const logEntrySchema: alepha22.TObject<{
347
+ level: alepha22.TUnsafe<"TRACE" | "SILENT" | "DEBUG" | "INFO" | "WARN" | "ERROR">;
348
+ message: alepha22.TString;
349
+ service: alepha22.TString;
350
+ module: alepha22.TString;
351
+ context: alepha22.TOptional<alepha22.TString>;
352
+ app: alepha22.TOptional<alepha22.TString>;
353
+ data: alepha22.TOptional<alepha22.TAny>;
354
+ timestamp: alepha22.TNumber;
352
355
  }>;
353
356
  type LogEntry = Static<typeof logEntrySchema>;
354
357
  //#endregion
@@ -363,8 +366,8 @@ declare class DateTimeProvider {
363
366
  protected readonly timeouts: Timeout[];
364
367
  protected readonly intervals: Interval[];
365
368
  constructor();
366
- protected readonly onStart: alepha30.HookPrimitive<"start">;
367
- protected readonly onStop: alepha30.HookPrimitive<"stop">;
369
+ protected readonly onStart: alepha22.HookPrimitive<"start">;
370
+ protected readonly onStop: alepha22.HookPrimitive<"stop">;
368
371
  setLocale(locale: string): void;
369
372
  isDateTime(value: unknown): value is DateTime;
370
373
  /**
@@ -496,7 +499,7 @@ declare class Logger implements LoggerInterface {
496
499
  }
497
500
  //#endregion
498
501
  //#region ../alepha/src/logger/index.d.ts
499
- declare const envSchema$2: alepha30.TObject<{
502
+ declare const envSchema$2: alepha22.TObject<{
500
503
  /**
501
504
  * Default log level for the application.
502
505
  *
@@ -513,14 +516,14 @@ declare const envSchema$2: alepha30.TObject<{
513
516
  * LOG_LEVEL=my.module.name:debug,info # Set debug level for my.module.name and info for all other modules
514
517
  * LOG_LEVEL=alepha:trace, info # Set trace level for all alepha modules and info for all other modules
515
518
  */
516
- LOG_LEVEL: alepha30.TOptional<alepha30.TString>;
519
+ LOG_LEVEL: alepha22.TOptional<alepha22.TString>;
517
520
  /**
518
521
  * Built-in log formats.
519
522
  * - "json" - JSON format, useful for structured logging and log aggregation. {@link JsonFormatterProvider}
520
523
  * - "pretty" - Simple text format, human-readable, with colors. {@link SimpleFormatterProvider}
521
524
  * - "raw" - Raw format, no formatting, just the message. {@link RawFormatterProvider}
522
525
  */
523
- LOG_FORMAT: alepha30.TOptional<alepha30.TUnsafe<"json" | "pretty" | "raw">>;
526
+ LOG_FORMAT: alepha22.TOptional<alepha22.TUnsafe<"json" | "pretty" | "raw">>;
524
527
  }>;
525
528
  declare module "alepha" {
526
529
  interface Env extends Partial<Static<typeof envSchema$2>> {}
@@ -557,8 +560,8 @@ declare class ServerTimingProvider {
557
560
  prefix: string;
558
561
  disabled: boolean;
559
562
  };
560
- readonly onRequest: alepha30.HookPrimitive<"server:onRequest">;
561
- readonly onResponse: alepha30.HookPrimitive<"server:onResponse">;
563
+ readonly onRequest: alepha22.HookPrimitive<"server:onRequest">;
564
+ readonly onResponse: alepha22.HookPrimitive<"server:onResponse">;
562
565
  protected get handlerName(): string;
563
566
  beginTiming(name: string): void;
564
567
  endTiming(name: string): void;
@@ -622,11 +625,11 @@ declare class ServerProvider {
622
625
  /**
623
626
  * When a Node.js HTTP request is received from outside. (Vercel, AWS Lambda, etc.)
624
627
  */
625
- protected readonly onNodeRequest: alepha30.HookPrimitive<"node:request">;
628
+ protected readonly onNodeRequest: alepha22.HookPrimitive<"node:request">;
626
629
  /**
627
630
  * When a Web (Fetch API) request is received from outside. (Netlify, Cloudflare Workers, etc.)
628
631
  */
629
- protected readonly onWebRequest: alepha30.HookPrimitive<"web:request">;
632
+ protected readonly onWebRequest: alepha22.HookPrimitive<"web:request">;
630
633
  /**
631
634
  * Handle Node.js HTTP request event.
632
635
  *
@@ -961,18 +964,18 @@ type ServerActionHandler<TConfig extends RequestConfigSchema = RequestConfigSche
961
964
  interface ServerActionRequest<TConfig extends RequestConfigSchema> extends ServerRequest<TConfig> {}
962
965
  //#endregion
963
966
  //#region ../alepha/src/server/providers/BunHttpServerProvider.d.ts
964
- declare const envSchema$1: alepha30.TObject<{
965
- SERVER_PORT: alepha30.TInteger;
966
- SERVER_HOST: alepha30.TString;
967
+ declare const envSchema$1: alepha22.TObject<{
968
+ SERVER_PORT: alepha22.TInteger;
969
+ SERVER_HOST: alepha22.TString;
967
970
  }>;
968
971
  declare module "alepha" {
969
972
  interface Env extends Partial<Static<typeof envSchema$1>> {}
970
973
  }
971
974
  //#endregion
972
975
  //#region ../alepha/src/server/providers/NodeHttpServerProvider.d.ts
973
- declare const envSchema: alepha30.TObject<{
974
- SERVER_PORT: alepha30.TInteger;
975
- SERVER_HOST: alepha30.TString;
976
+ declare const envSchema: alepha22.TObject<{
977
+ SERVER_PORT: alepha22.TInteger;
978
+ SERVER_HOST: alepha22.TString;
976
979
  }>;
977
980
  declare module "alepha" {
978
981
  interface Env extends Partial<Static<typeof envSchema>> {}
@@ -1038,7 +1041,7 @@ declare module "alepha" {
1038
1041
  declare class ServerHeadProvider {
1039
1042
  protected readonly headProvider: HeadProvider;
1040
1043
  protected readonly serverTimingProvider: ServerTimingProvider;
1041
- protected readonly onServerRenderEnd: alepha30.HookPrimitive<"react:server:render:end">;
1044
+ protected readonly onServerRenderEnd: alepha22.HookPrimitive<"react:server:render:end">;
1042
1045
  renderHead(template: string, head: SimpleHead): string;
1043
1046
  protected mergeAttributes(existing: string, attrs: Record<string, string>): string;
1044
1047
  protected parseAttributes(attrStr: string): Record<string, string>;
@@ -1060,7 +1063,7 @@ declare module "@alepha/react" {
1060
1063
  * @see {@link ServerHeadProvider}
1061
1064
  * @module alepha.react.head
1062
1065
  */
1063
- declare const AlephaReactHead: alepha30.Service<alepha30.Module>;
1066
+ declare const AlephaReactHead: alepha22.Service<alepha22.Module>;
1064
1067
  //#endregion
1065
1068
  export { $head, AlephaReactHead, Head, HeadPrimitive, HeadPrimitiveOptions, ServerHeadProvider, SimpleHead, UseHeadOptions, UseHeadReturn, useHead };
1066
1069
  //# sourceMappingURL=index.d.ts.map
@@ -5,16 +5,17 @@ import { useCallback, useEffect, useMemo } from "react";
5
5
 
6
6
  //#region src/head/providers/HeadProvider.ts
7
7
  var HeadProvider = class {
8
- global;
9
- getGlobalHead() {
10
- if (typeof this.global === "function") return this.global();
11
- return this.global;
12
- }
8
+ global = [];
13
9
  fillHead(state) {
14
- state.head = {
15
- ...state.head,
16
- ...this.getGlobalHead()
17
- };
10
+ state.head = { ...state.head };
11
+ for (const h of this.global ?? []) {
12
+ const head = typeof h === "function" ? h() : h;
13
+ state.head = {
14
+ ...state.head,
15
+ ...head,
16
+ meta: [...state.head.meta ?? [], ...head.meta ?? []]
17
+ };
18
+ }
18
19
  for (const layer of state.layers) if (layer.route?.head && !layer.error) this.fillHeadByPage(layer.route, state, layer.props ?? {});
19
20
  }
20
21
  fillHeadByPage(page, state, props) {
@@ -50,7 +51,7 @@ const $head = (options) => {
50
51
  var HeadPrimitive = class extends Primitive {
51
52
  provider = $inject(HeadProvider);
52
53
  onInit() {
53
- this.provider.global = this.options;
54
+ this.provider.global = [...this.provider.global ?? [], this.options];
54
55
  }
55
56
  };
56
57
  $head[KIND] = HeadPrimitive;
@@ -80,6 +81,7 @@ var ServerHeadProvider = class {
80
81
  if (title) if (template.includes("<title>")) result = result.replace(/<title>(.*?)<\/title>/i, () => `<title>${this.escapeHtml(title)}</title>`);
81
82
  else headContent += `<title>${this.escapeHtml(title)}</title>\n`;
82
83
  if (head.meta) for (const meta of head.meta) headContent += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
84
+ if (head.link) for (const link of head.link) headContent += `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}">\n`;
83
85
  result = result.replace(/<head([^>]*)>(.*?)<\/head>/is, (_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`);
84
86
  return result.trim();
85
87
  }
@@ -173,6 +175,16 @@ var BrowserHeadProvider = class {
173
175
  document.head.appendChild(newMeta);
174
176
  }
175
177
  }
178
+ if (head.link) for (const it of head.link) {
179
+ const { rel, href } = it;
180
+ let link = document.querySelector(`link[rel="${rel}"][href="${href}"]`);
181
+ if (!link) {
182
+ link = document.createElement("link");
183
+ link.setAttribute("rel", rel);
184
+ link.setAttribute("href", href);
185
+ document.head.appendChild(link);
186
+ }
187
+ }
176
188
  }
177
189
  };
178
190
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["attrs: Record<string, string>","match: RegExpExecArray | null","attrs: Record<string, string>","metas: { name: string; content: string }[]"],"sources":["../../src/head/providers/HeadProvider.ts","../../src/head/primitives/$head.ts","../../src/head/providers/ServerHeadProvider.ts","../../src/head/providers/BrowserHeadProvider.ts","../../src/head/hooks/useHead.ts","../../src/head/index.ts"],"sourcesContent":["import type { PageRoute, ReactRouterState } from \"@alepha/react\";\nimport type { Head } from \"../interfaces/Head.ts\";\n\nexport class HeadProvider {\n public global?: Head | (() => Head);\n\n protected getGlobalHead(): Head | undefined {\n if (typeof this.global === \"function\") {\n return this.global();\n }\n return this.global;\n }\n\n public fillHead(state: ReactRouterState) {\n state.head = {\n ...state.head,\n ...this.getGlobalHead(),\n };\n\n for (const layer of state.layers) {\n if (layer.route?.head && !layer.error) {\n this.fillHeadByPage(layer.route, state, layer.props ?? {});\n }\n }\n }\n\n protected fillHeadByPage(\n page: PageRoute,\n state: ReactRouterState,\n props: Record<string, any>,\n ): void {\n if (!page.head) {\n return;\n }\n\n state.head ??= {};\n\n const head =\n typeof page.head === \"function\"\n ? page.head(props, state.head)\n : page.head;\n\n if (head.title) {\n state.head ??= {};\n\n if (state.head.titleSeparator) {\n state.head.title = `${head.title}${state.head.titleSeparator}${state.head.title}`;\n } else {\n state.head.title = head.title;\n }\n\n state.head.titleSeparator = head.titleSeparator;\n }\n\n if (head.htmlAttributes) {\n state.head.htmlAttributes = {\n ...state.head.htmlAttributes,\n ...head.htmlAttributes,\n };\n }\n\n if (head.bodyAttributes) {\n state.head.bodyAttributes = {\n ...state.head.bodyAttributes,\n ...head.bodyAttributes,\n };\n }\n\n if (head.meta) {\n state.head.meta = [...(state.head.meta ?? []), ...(head.meta ?? [])];\n }\n }\n}\n","import { $inject, createPrimitive, Primitive, KIND } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"../providers/HeadProvider.ts\";\n\n/**\n * Set global `<head>` options for the application.\n */\nexport const $head = (options: HeadPrimitiveOptions) => {\n return createPrimitive(HeadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type HeadPrimitiveOptions = Head | (() => Head);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class HeadPrimitive extends Primitive<HeadPrimitiveOptions> {\n protected readonly provider = $inject(HeadProvider);\n protected onInit() {\n this.provider.global = this.options;\n }\n}\n\n$head[KIND] = HeadPrimitive;\n","import { $hook, $inject } from \"alepha\";\nimport { ServerTimingProvider } from \"alepha/server\";\nimport type { SimpleHead } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class ServerHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n protected readonly serverTimingProvider = $inject(ServerTimingProvider);\n\n protected readonly onServerRenderEnd = $hook({\n on: \"react:server:render:end\",\n handler: async (ev) => {\n this.serverTimingProvider.beginTiming(\"renderHead\");\n this.headProvider.fillHead(ev.state);\n if (ev.state.head) {\n ev.html = this.renderHead(ev.html, ev.state.head);\n }\n this.serverTimingProvider.endTiming(\"renderHead\");\n },\n });\n\n public renderHead(template: string, head: SimpleHead): string {\n let result = template;\n\n // Inject htmlAttributes\n const htmlAttributes = head.htmlAttributes;\n if (htmlAttributes) {\n result = result.replace(\n /<html([^>]*)>/i,\n (_, existingAttrs) =>\n `<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`,\n );\n }\n\n // Inject bodyAttributes\n const bodyAttributes = head.bodyAttributes;\n if (bodyAttributes) {\n result = result.replace(\n /<body([^>]*)>/i,\n (_, existingAttrs) =>\n `<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`,\n );\n }\n\n // Build head content\n let headContent = \"\";\n const title = head.title;\n if (title) {\n if (template.includes(\"<title>\")) {\n result = result.replace(\n /<title>(.*?)<\\/title>/i,\n () => `<title>${this.escapeHtml(title)}</title>`,\n );\n } else {\n headContent += `<title>${this.escapeHtml(title)}</title>\\n`;\n }\n }\n\n if (head.meta) {\n for (const meta of head.meta) {\n headContent += `<meta name=\"${this.escapeHtml(meta.name)}\" content=\"${this.escapeHtml(meta.content)}\">\\n`;\n }\n }\n\n // Inject into <head>...</head>\n result = result.replace(\n /<head([^>]*)>(.*?)<\\/head>/is,\n (_, existingAttrs, existingHead) =>\n `<head${existingAttrs}>${existingHead}${headContent}</head>`,\n );\n\n return result.trim();\n }\n\n protected mergeAttributes(\n existing: string,\n attrs: Record<string, string>,\n ): string {\n const existingAttrs = this.parseAttributes(existing);\n const merged = { ...existingAttrs, ...attrs };\n return Object.entries(merged)\n .map(([k, v]) => ` ${k}=\"${this.escapeHtml(v)}\"`)\n .join(\"\");\n }\n\n protected parseAttributes(attrStr: string): Record<string, string> {\n attrStr = attrStr.replaceAll(\"'\", '\"');\n\n const attrs: Record<string, string> = {};\n const attrRegex = /([^\\s=]+)(?:=\"([^\"]*)\")?/g;\n let match: RegExpExecArray | null = attrRegex.exec(attrStr);\n\n while (match) {\n attrs[match[1]] = match[2] ?? \"\";\n match = attrRegex.exec(attrStr);\n }\n\n return attrs;\n }\n\n protected escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n }\n}\n","import { $hook, $inject } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class BrowserHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n\n protected get document(): Document {\n return window.document;\n }\n\n protected readonly onBrowserRender = $hook({\n on: \"react:browser:render\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n protected readonly onTransitionEnd = $hook({\n on: \"react:transition:end\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n public getHead(document: Document): Head {\n return {\n get title() {\n return document.title;\n },\n get htmlAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.documentElement.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get bodyAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.body.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get meta() {\n const metas: { name: string; content: string }[] = [];\n for (const meta of document.head.querySelectorAll(\"meta[name]\")) {\n const name = meta.getAttribute(\"name\");\n const content = meta.getAttribute(\"content\");\n if (name && content) {\n metas.push({ name, content });\n }\n }\n return metas;\n },\n };\n }\n\n public renderHead(document: Document, head: Head): void {\n if (head.title) {\n document.title = head.title;\n }\n\n if (head.bodyAttributes) {\n for (const [key, value] of Object.entries(head.bodyAttributes)) {\n if (value) {\n document.body.setAttribute(key, value);\n } else {\n document.body.removeAttribute(key);\n }\n }\n }\n\n if (head.htmlAttributes) {\n for (const [key, value] of Object.entries(head.htmlAttributes)) {\n if (value) {\n document.documentElement.setAttribute(key, value);\n } else {\n document.documentElement.removeAttribute(key);\n }\n }\n }\n\n if (head.meta) {\n for (const it of head.meta) {\n const { name, content } = it;\n const meta = document.querySelector(`meta[name=\"${name}\"]`);\n if (meta) {\n meta.setAttribute(\"content\", content);\n } else {\n const newMeta = document.createElement(\"meta\");\n newMeta.setAttribute(\"name\", name);\n newMeta.setAttribute(\"content\", content);\n document.head.appendChild(newMeta);\n }\n }\n }\n }\n}\n","import { useInject } from \"@alepha/react\";\nimport { Alepha } from \"alepha\";\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { BrowserHeadProvider } from \"../providers/BrowserHeadProvider.ts\";\n\n/**\n * ```tsx\n * const App = () => {\n * const [head, setHead] = useHead({\n * // will set the document title on the first render\n * title: \"My App\",\n * });\n *\n * return (\n * // This will update the document title when the button is clicked\n * <button onClick={() => setHead({ title: \"Change Title\" })}>\n * Change Title {head.title}\n * </button>\n * );\n * }\n * ```\n */\nexport const useHead = (options?: UseHeadOptions): UseHeadReturn => {\n const alepha = useInject(Alepha);\n\n const current = useMemo(() => {\n if (!alepha.isBrowser()) {\n return {};\n }\n\n return alepha.inject(BrowserHeadProvider).getHead(window.document);\n }, []);\n\n const setHead = useCallback((head?: Head | ((previous?: Head) => Head)) => {\n if (!alepha.isBrowser()) {\n return;\n }\n\n alepha\n .inject(BrowserHeadProvider)\n .renderHead(\n window.document,\n typeof head === \"function\" ? head(current) : head || {},\n );\n }, []);\n\n useEffect(() => {\n if (options) {\n setHead(options);\n }\n }, []);\n\n return [current, setHead];\n};\n\nexport type UseHeadOptions = Head | ((previous?: Head) => Head);\n\nexport type UseHeadReturn = [\n Head,\n (head?: Head | ((previous?: Head) => Head)) => void,\n];\n","import {\n AlephaReact,\n type PageConfigSchema,\n type TPropsDefault,\n type TPropsParentDefault,\n} from \"@alepha/react\";\nimport { $module } from \"alepha\";\nimport { $head } from \"./primitives/$head.ts\";\nimport type { Head } from \"./interfaces/Head.ts\";\nimport { ServerHeadProvider } from \"./providers/ServerHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$head.ts\";\nexport * from \"./hooks/useHead.ts\";\nexport * from \"./interfaces/Head.ts\";\nexport * from \"./providers/ServerHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"@alepha/react\" {\n interface PagePrimitiveOptions<\n TConfig extends PageConfigSchema = PageConfigSchema,\n TProps extends object = TPropsDefault,\n TPropsParent extends object = TPropsParentDefault,\n > {\n head?: Head | ((props: TProps, previous?: Head) => Head);\n }\n\n interface ReactRouterState {\n head: Head;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Fill `<head>` server & client side.\n *\n * @see {@link ServerHeadProvider}\n * @module alepha.react.head\n */\nexport const AlephaReactHead = $module({\n name: \"alepha.react.head\",\n primitives: [$head],\n services: [AlephaReact, ServerHeadProvider],\n});\n"],"mappings":";;;;;;AAGA,IAAa,eAAb,MAA0B;CACxB,AAAO;CAEP,AAAU,gBAAkC;AAC1C,MAAI,OAAO,KAAK,WAAW,WACzB,QAAO,KAAK,QAAQ;AAEtB,SAAO,KAAK;;CAGd,AAAO,SAAS,OAAyB;AACvC,QAAM,OAAO;GACX,GAAG,MAAM;GACT,GAAG,KAAK,eAAe;GACxB;AAED,OAAK,MAAM,SAAS,MAAM,OACxB,KAAI,MAAM,OAAO,QAAQ,CAAC,MAAM,MAC9B,MAAK,eAAe,MAAM,OAAO,OAAO,MAAM,SAAS,EAAE,CAAC;;CAKhE,AAAU,eACR,MACA,OACA,OACM;AACN,MAAI,CAAC,KAAK,KACR;AAGF,QAAM,SAAS,EAAE;EAEjB,MAAM,OACJ,OAAO,KAAK,SAAS,aACjB,KAAK,KAAK,OAAO,MAAM,KAAK,GAC5B,KAAK;AAEX,MAAI,KAAK,OAAO;AACd,SAAM,SAAS,EAAE;AAEjB,OAAI,MAAM,KAAK,eACb,OAAM,KAAK,QAAQ,GAAG,KAAK,QAAQ,MAAM,KAAK,iBAAiB,MAAM,KAAK;OAE1E,OAAM,KAAK,QAAQ,KAAK;AAG1B,SAAM,KAAK,iBAAiB,KAAK;;AAGnC,MAAI,KAAK,eACP,OAAM,KAAK,iBAAiB;GAC1B,GAAG,MAAM,KAAK;GACd,GAAG,KAAK;GACT;AAGH,MAAI,KAAK,eACP,OAAM,KAAK,iBAAiB;GAC1B,GAAG,MAAM,KAAK;GACd,GAAG,KAAK;GACT;AAGH,MAAI,KAAK,KACP,OAAM,KAAK,OAAO,CAAC,GAAI,MAAM,KAAK,QAAQ,EAAE,EAAG,GAAI,KAAK,QAAQ,EAAE,CAAE;;;;;;;;;AC9D1E,MAAa,SAAS,YAAkC;AACtD,QAAO,gBAAgB,eAAe,QAAQ;;AAShD,IAAa,gBAAb,cAAmC,UAAgC;CACjE,AAAmB,WAAW,QAAQ,aAAa;CACnD,AAAU,SAAS;AACjB,OAAK,SAAS,SAAS,KAAK;;;AAIhC,MAAM,QAAQ;;;;ACnBd,IAAa,qBAAb,MAAgC;CAC9B,AAAmB,eAAe,QAAQ,aAAa;CACvD,AAAmB,uBAAuB,QAAQ,qBAAqB;CAEvE,AAAmB,oBAAoB,MAAM;EAC3C,IAAI;EACJ,SAAS,OAAO,OAAO;AACrB,QAAK,qBAAqB,YAAY,aAAa;AACnD,QAAK,aAAa,SAAS,GAAG,MAAM;AACpC,OAAI,GAAG,MAAM,KACX,IAAG,OAAO,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,KAAK;AAEnD,QAAK,qBAAqB,UAAU,aAAa;;EAEpD,CAAC;CAEF,AAAO,WAAW,UAAkB,MAA0B;EAC5D,IAAI,SAAS;EAGb,MAAM,iBAAiB,KAAK;AAC5B,MAAI,eACF,UAAS,OAAO,QACd,mBACC,GAAG,kBACF,QAAQ,KAAK,gBAAgB,eAAe,eAAe,CAAC,GAC/D;EAIH,MAAM,iBAAiB,KAAK;AAC5B,MAAI,eACF,UAAS,OAAO,QACd,mBACC,GAAG,kBACF,QAAQ,KAAK,gBAAgB,eAAe,eAAe,CAAC,GAC/D;EAIH,IAAI,cAAc;EAClB,MAAM,QAAQ,KAAK;AACnB,MAAI,MACF,KAAI,SAAS,SAAS,UAAU,CAC9B,UAAS,OAAO,QACd,gCACM,UAAU,KAAK,WAAW,MAAM,CAAC,UACxC;MAED,gBAAe,UAAU,KAAK,WAAW,MAAM,CAAC;AAIpD,MAAI,KAAK,KACP,MAAK,MAAM,QAAQ,KAAK,KACtB,gBAAe,eAAe,KAAK,WAAW,KAAK,KAAK,CAAC,aAAa,KAAK,WAAW,KAAK,QAAQ,CAAC;AAKxG,WAAS,OAAO,QACd,iCACC,GAAG,eAAe,iBACjB,QAAQ,cAAc,GAAG,eAAe,YAAY,SACvD;AAED,SAAO,OAAO,MAAM;;CAGtB,AAAU,gBACR,UACA,OACQ;EAER,MAAM,SAAS;GAAE,GADK,KAAK,gBAAgB,SAAS;GACjB,GAAG;GAAO;AAC7C,SAAO,OAAO,QAAQ,OAAO,CAC1B,KAAK,CAAC,GAAG,OAAO,IAAI,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC,GAAG,CAChD,KAAK,GAAG;;CAGb,AAAU,gBAAgB,SAAyC;AACjE,YAAU,QAAQ,WAAW,KAAK,KAAI;EAEtC,MAAMA,QAAgC,EAAE;EACxC,MAAM,YAAY;EAClB,IAAIC,QAAgC,UAAU,KAAK,QAAQ;AAE3D,SAAO,OAAO;AACZ,SAAM,MAAM,MAAM,MAAM,MAAM;AAC9B,WAAQ,UAAU,KAAK,QAAQ;;AAGjC,SAAO;;CAGT,AAAU,WAAW,KAAqB;AACxC,SAAO,IACJ,QAAQ,MAAM,QAAQ,CACtB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,OAAO,CACrB,QAAQ,MAAM,SAAS,CACvB,QAAQ,MAAM,SAAS;;;;;;ACtG9B,IAAa,sBAAb,MAAiC;CAC/B,AAAmB,eAAe,QAAQ,aAAa;CAEvD,IAAc,WAAqB;AACjC,SAAO,OAAO;;CAGhB,AAAmB,kBAAkB,MAAM;EACzC,IAAI;EACJ,SAAS,OAAO,EAAE,YAAY;AAC5B,QAAK,aAAa,SAAS,MAAM;AACjC,OAAI,MAAM,KACR,MAAK,WAAW,KAAK,UAAU,MAAM,KAAK;;EAG/C,CAAC;CAEF,AAAmB,kBAAkB,MAAM;EACzC,IAAI;EACJ,SAAS,OAAO,EAAE,YAAY;AAC5B,QAAK,aAAa,SAAS,MAAM;AACjC,OAAI,MAAM,KACR,MAAK,WAAW,KAAK,UAAU,MAAM,KAAK;;EAG/C,CAAC;CAEF,AAAO,QAAQ,UAA0B;AACvC,SAAO;GACL,IAAI,QAAQ;AACV,WAAO,SAAS;;GAElB,IAAI,iBAAiB;IACnB,MAAMC,QAAgC,EAAE;AACxC,SAAK,MAAM,QAAQ,SAAS,gBAAgB,WAC1C,OAAM,KAAK,QAAQ,KAAK;AAE1B,WAAO;;GAET,IAAI,iBAAiB;IACnB,MAAMA,QAAgC,EAAE;AACxC,SAAK,MAAM,QAAQ,SAAS,KAAK,WAC/B,OAAM,KAAK,QAAQ,KAAK;AAE1B,WAAO;;GAET,IAAI,OAAO;IACT,MAAMC,QAA6C,EAAE;AACrD,SAAK,MAAM,QAAQ,SAAS,KAAK,iBAAiB,aAAa,EAAE;KAC/D,MAAM,OAAO,KAAK,aAAa,OAAO;KACtC,MAAM,UAAU,KAAK,aAAa,UAAU;AAC5C,SAAI,QAAQ,QACV,OAAM,KAAK;MAAE;MAAM;MAAS,CAAC;;AAGjC,WAAO;;GAEV;;CAGH,AAAO,WAAW,UAAoB,MAAkB;AACtD,MAAI,KAAK,MACP,UAAS,QAAQ,KAAK;AAGxB,MAAI,KAAK,eACP,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,eAAe,CAC5D,KAAI,MACF,UAAS,KAAK,aAAa,KAAK,MAAM;MAEtC,UAAS,KAAK,gBAAgB,IAAI;AAKxC,MAAI,KAAK,eACP,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,eAAe,CAC5D,KAAI,MACF,UAAS,gBAAgB,aAAa,KAAK,MAAM;MAEjD,UAAS,gBAAgB,gBAAgB,IAAI;AAKnD,MAAI,KAAK,KACP,MAAK,MAAM,MAAM,KAAK,MAAM;GAC1B,MAAM,EAAE,MAAM,YAAY;GAC1B,MAAM,OAAO,SAAS,cAAc,cAAc,KAAK,IAAI;AAC3D,OAAI,KACF,MAAK,aAAa,WAAW,QAAQ;QAChC;IACL,MAAM,UAAU,SAAS,cAAc,OAAO;AAC9C,YAAQ,aAAa,QAAQ,KAAK;AAClC,YAAQ,aAAa,WAAW,QAAQ;AACxC,aAAS,KAAK,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;AC5E5C,MAAa,WAAW,YAA4C;CAClE,MAAM,SAAS,UAAU,OAAO;CAEhC,MAAM,UAAU,cAAc;AAC5B,MAAI,CAAC,OAAO,WAAW,CACrB,QAAO,EAAE;AAGX,SAAO,OAAO,OAAO,oBAAoB,CAAC,QAAQ,OAAO,SAAS;IACjE,EAAE,CAAC;CAEN,MAAM,UAAU,aAAa,SAA8C;AACzE,MAAI,CAAC,OAAO,WAAW,CACrB;AAGF,SACG,OAAO,oBAAoB,CAC3B,WACC,OAAO,UACP,OAAO,SAAS,aAAa,KAAK,QAAQ,GAAG,QAAQ,EAAE,CACxD;IACF,EAAE,CAAC;AAEN,iBAAgB;AACd,MAAI,QACF,SAAQ,QAAQ;IAEjB,EAAE,CAAC;AAEN,QAAO,CAAC,SAAS,QAAQ;;;;;;;;;;;ACX3B,MAAa,kBAAkB,QAAQ;CACrC,MAAM;CACN,YAAY,CAAC,MAAM;CACnB,UAAU,CAAC,aAAa,mBAAmB;CAC5C,CAAC"}
1
+ {"version":3,"file":"index.js","names":["attrs: Record<string, string>","match: RegExpExecArray | null","attrs: Record<string, string>","metas: { name: string; content: string }[]"],"sources":["../../src/head/providers/HeadProvider.ts","../../src/head/primitives/$head.ts","../../src/head/providers/ServerHeadProvider.ts","../../src/head/providers/BrowserHeadProvider.ts","../../src/head/hooks/useHead.ts","../../src/head/index.ts"],"sourcesContent":["import type { PageRoute, ReactRouterState } from \"@alepha/react\";\nimport type { Head } from \"../interfaces/Head.ts\";\n\nexport class HeadProvider {\n public global?: Array<Head | (() => Head)> = [];\n\n public fillHead(state: ReactRouterState) {\n state.head = {\n ...state.head,\n };\n\n for (const h of this.global ?? []) {\n const head =\n typeof h === \"function\" ? h() : h;\n state.head = {\n ...state.head,\n ...head,\n meta: [...(state.head.meta ?? []), ...(head.meta ?? [])],\n };\n }\n\n for (const layer of state.layers) {\n if (layer.route?.head && !layer.error) {\n this.fillHeadByPage(layer.route, state, layer.props ?? {});\n }\n }\n }\n\n protected fillHeadByPage(\n page: PageRoute,\n state: ReactRouterState,\n props: Record<string, any>,\n ): void {\n if (!page.head) {\n return;\n }\n\n state.head ??= {};\n\n const head =\n typeof page.head === \"function\"\n ? page.head(props, state.head)\n : page.head;\n\n if (head.title) {\n state.head ??= {};\n\n if (state.head.titleSeparator) {\n state.head.title = `${head.title}${state.head.titleSeparator}${state.head.title}`;\n } else {\n state.head.title = head.title;\n }\n\n state.head.titleSeparator = head.titleSeparator;\n }\n\n if (head.htmlAttributes) {\n state.head.htmlAttributes = {\n ...state.head.htmlAttributes,\n ...head.htmlAttributes,\n };\n }\n\n if (head.bodyAttributes) {\n state.head.bodyAttributes = {\n ...state.head.bodyAttributes,\n ...head.bodyAttributes,\n };\n }\n\n if (head.meta) {\n state.head.meta = [...(state.head.meta ?? []), ...(head.meta ?? [])];\n }\n }\n}\n","import { $inject, createPrimitive, Primitive, KIND } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"../providers/HeadProvider.ts\";\n\n/**\n * Set global `<head>` options for the application.\n */\nexport const $head = (options: HeadPrimitiveOptions) => {\n return createPrimitive(HeadPrimitive, options);\n};\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport type HeadPrimitiveOptions = Head | (() => Head);\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class HeadPrimitive extends Primitive<HeadPrimitiveOptions> {\n protected readonly provider = $inject(HeadProvider);\n protected onInit() {\n this.provider.global = [\n ...(this.provider.global ?? []),\n this.options,\n ];\n }\n}\n\n$head[KIND] = HeadPrimitive;\n","import { $hook, $inject } from \"alepha\";\nimport { ServerTimingProvider } from \"alepha/server\";\nimport type { SimpleHead } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class ServerHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n protected readonly serverTimingProvider = $inject(ServerTimingProvider);\n\n protected readonly onServerRenderEnd = $hook({\n on: \"react:server:render:end\",\n handler: async (ev) => {\n this.serverTimingProvider.beginTiming(\"renderHead\");\n this.headProvider.fillHead(ev.state);\n if (ev.state.head) {\n ev.html = this.renderHead(ev.html, ev.state.head);\n }\n this.serverTimingProvider.endTiming(\"renderHead\");\n },\n });\n\n public renderHead(template: string, head: SimpleHead): string {\n let result = template;\n\n // Inject htmlAttributes\n const htmlAttributes = head.htmlAttributes;\n if (htmlAttributes) {\n result = result.replace(\n /<html([^>]*)>/i,\n (_, existingAttrs) =>\n `<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`,\n );\n }\n\n // Inject bodyAttributes\n const bodyAttributes = head.bodyAttributes;\n if (bodyAttributes) {\n result = result.replace(\n /<body([^>]*)>/i,\n (_, existingAttrs) =>\n `<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`,\n );\n }\n\n // Build head content\n let headContent = \"\";\n const title = head.title;\n if (title) {\n if (template.includes(\"<title>\")) {\n result = result.replace(\n /<title>(.*?)<\\/title>/i,\n () => `<title>${this.escapeHtml(title)}</title>`,\n );\n } else {\n headContent += `<title>${this.escapeHtml(title)}</title>\\n`;\n }\n }\n\n if (head.meta) {\n for (const meta of head.meta) {\n headContent += `<meta name=\"${this.escapeHtml(meta.name)}\" content=\"${this.escapeHtml(meta.content)}\">\\n`;\n }\n }\n\n if (head.link) {\n for (const link of head.link) {\n headContent += `<link rel=\"${this.escapeHtml(link.rel)}\" href=\"${this.escapeHtml(link.href)}\">\\n`;\n }\n }\n\n // Inject into <head>...</head>\n result = result.replace(\n /<head([^>]*)>(.*?)<\\/head>/is,\n (_, existingAttrs, existingHead) =>\n `<head${existingAttrs}>${existingHead}${headContent}</head>`,\n );\n\n return result.trim();\n }\n\n protected mergeAttributes(\n existing: string,\n attrs: Record<string, string>,\n ): string {\n const existingAttrs = this.parseAttributes(existing);\n const merged = { ...existingAttrs, ...attrs };\n return Object.entries(merged)\n .map(([k, v]) => ` ${k}=\"${this.escapeHtml(v)}\"`)\n .join(\"\");\n }\n\n protected parseAttributes(attrStr: string): Record<string, string> {\n attrStr = attrStr.replaceAll(\"'\", '\"');\n\n const attrs: Record<string, string> = {};\n const attrRegex = /([^\\s=]+)(?:=\"([^\"]*)\")?/g;\n let match: RegExpExecArray | null = attrRegex.exec(attrStr);\n\n while (match) {\n attrs[match[1]] = match[2] ?? \"\";\n match = attrRegex.exec(attrStr);\n }\n\n return attrs;\n }\n\n protected escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n }\n}\n","import { $hook, $inject } from \"alepha\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { HeadProvider } from \"./HeadProvider.ts\";\n\nexport class BrowserHeadProvider {\n protected readonly headProvider = $inject(HeadProvider);\n\n protected get document(): Document {\n return window.document;\n }\n\n protected readonly onBrowserRender = $hook({\n on: \"react:browser:render\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n protected readonly onTransitionEnd = $hook({\n on: \"react:transition:end\",\n handler: async ({ state }) => {\n this.headProvider.fillHead(state);\n if (state.head) {\n this.renderHead(this.document, state.head);\n }\n },\n });\n\n public getHead(document: Document): Head {\n return {\n get title() {\n return document.title;\n },\n get htmlAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.documentElement.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get bodyAttributes() {\n const attrs: Record<string, string> = {};\n for (const attr of document.body.attributes) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n },\n get meta() {\n const metas: { name: string; content: string }[] = [];\n for (const meta of document.head.querySelectorAll(\"meta[name]\")) {\n const name = meta.getAttribute(\"name\");\n const content = meta.getAttribute(\"content\");\n if (name && content) {\n metas.push({ name, content });\n }\n }\n return metas;\n },\n };\n }\n\n public renderHead(document: Document, head: Head): void {\n if (head.title) {\n document.title = head.title;\n }\n\n if (head.bodyAttributes) {\n for (const [key, value] of Object.entries(head.bodyAttributes)) {\n if (value) {\n document.body.setAttribute(key, value);\n } else {\n document.body.removeAttribute(key);\n }\n }\n }\n\n if (head.htmlAttributes) {\n for (const [key, value] of Object.entries(head.htmlAttributes)) {\n if (value) {\n document.documentElement.setAttribute(key, value);\n } else {\n document.documentElement.removeAttribute(key);\n }\n }\n }\n\n if (head.meta) {\n for (const it of head.meta) {\n const { name, content } = it;\n const meta = document.querySelector(`meta[name=\"${name}\"]`);\n if (meta) {\n meta.setAttribute(\"content\", content);\n } else {\n const newMeta = document.createElement(\"meta\");\n newMeta.setAttribute(\"name\", name);\n newMeta.setAttribute(\"content\", content);\n document.head.appendChild(newMeta);\n }\n }\n }\n\n if (head.link) {\n for (const it of head.link) {\n const { rel, href } = it;\n let link = document.querySelector(`link[rel=\"${rel}\"][href=\"${href}\"]`);\n if (!link) {\n link = document.createElement(\"link\");\n link.setAttribute(\"rel\", rel);\n link.setAttribute(\"href\", href);\n document.head.appendChild(link);\n }\n }\n }\n }\n}\n","import { useInject } from \"@alepha/react\";\nimport { Alepha } from \"alepha\";\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport type { Head } from \"../interfaces/Head.ts\";\nimport { BrowserHeadProvider } from \"../providers/BrowserHeadProvider.ts\";\n\n/**\n * ```tsx\n * const App = () => {\n * const [head, setHead] = useHead({\n * // will set the document title on the first render\n * title: \"My App\",\n * });\n *\n * return (\n * // This will update the document title when the button is clicked\n * <button onClick={() => setHead({ title: \"Change Title\" })}>\n * Change Title {head.title}\n * </button>\n * );\n * }\n * ```\n */\nexport const useHead = (options?: UseHeadOptions): UseHeadReturn => {\n const alepha = useInject(Alepha);\n\n const current = useMemo(() => {\n if (!alepha.isBrowser()) {\n return {};\n }\n\n return alepha.inject(BrowserHeadProvider).getHead(window.document);\n }, []);\n\n const setHead = useCallback((head?: Head | ((previous?: Head) => Head)) => {\n if (!alepha.isBrowser()) {\n return;\n }\n\n alepha\n .inject(BrowserHeadProvider)\n .renderHead(\n window.document,\n typeof head === \"function\" ? head(current) : head || {},\n );\n }, []);\n\n useEffect(() => {\n if (options) {\n setHead(options);\n }\n }, []);\n\n return [current, setHead];\n};\n\nexport type UseHeadOptions = Head | ((previous?: Head) => Head);\n\nexport type UseHeadReturn = [\n Head,\n (head?: Head | ((previous?: Head) => Head)) => void,\n];\n","import {\n AlephaReact,\n type PageConfigSchema,\n type TPropsDefault,\n type TPropsParentDefault,\n} from \"@alepha/react\";\nimport { $module } from \"alepha\";\nimport { $head } from \"./primitives/$head.ts\";\nimport type { Head } from \"./interfaces/Head.ts\";\nimport { ServerHeadProvider } from \"./providers/ServerHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./primitives/$head.ts\";\nexport * from \"./hooks/useHead.ts\";\nexport * from \"./interfaces/Head.ts\";\nexport * from \"./providers/ServerHeadProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"@alepha/react\" {\n interface PagePrimitiveOptions<\n TConfig extends PageConfigSchema = PageConfigSchema,\n TProps extends object = TPropsDefault,\n TPropsParent extends object = TPropsParentDefault,\n > {\n head?: Head | ((props: TProps, previous?: Head) => Head);\n }\n\n interface ReactRouterState {\n head: Head;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Fill `<head>` server & client side.\n *\n * @see {@link ServerHeadProvider}\n * @module alepha.react.head\n */\nexport const AlephaReactHead = $module({\n name: \"alepha.react.head\",\n primitives: [$head],\n services: [AlephaReact, ServerHeadProvider],\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@alepha/react",
3
3
  "description": "React components and hooks for building Alepha applications.",
4
4
  "author": "Nicolas Foures",
5
- "version": "0.13.2",
5
+ "version": "0.13.3",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
@@ -18,20 +18,20 @@
18
18
  "bin": "./dist/index.js",
19
19
  "dependencies": {
20
20
  "@vitejs/plugin-react": "^5.1.1",
21
- "react-dom": "^19.2.0"
21
+ "react-dom": "^19.2.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@testing-library/dom": "^10.4.1",
25
25
  "@testing-library/react": "^16.3.0",
26
26
  "@types/react": "^19.2.7",
27
27
  "@types/react-dom": "^19.2.3",
28
- "alepha": "0.13.2",
28
+ "alepha": "0.13.3",
29
29
  "jsdom": "^27.2.0",
30
- "react": "^19.2.0",
31
- "vitest": "^4.0.14"
30
+ "react": "^19.2.1",
31
+ "vitest": "^4.0.15"
32
32
  },
33
33
  "peerDependencies": {
34
- "alepha": "0.13.2",
34
+ "alepha": "0.13.3",
35
35
  "react": "^19.2.0"
36
36
  },
37
37
  "scripts": {
@@ -4,6 +4,7 @@ import { $logger } from "alepha/logger";
4
4
  import type { UserAccountToken } from "alepha/security";
5
5
  import { HttpClient } from "alepha/server";
6
6
  import { alephaServerAuthRoutes, tokenResponseSchema, type Tokens, userinfoResponseSchema } from "alepha/server/auth";
7
+ import { LinkProvider } from "alepha/server/links";
7
8
 
8
9
  /**
9
10
  * Browser, SSR friendly, service to handle authentication.
@@ -12,6 +13,7 @@ export class ReactAuth {
12
13
  protected readonly log = $logger();
13
14
  protected readonly alepha = $inject(Alepha);
14
15
  protected readonly httpClient = $inject(HttpClient);
16
+ protected readonly linkProvider = $inject(LinkProvider);
15
17
 
16
18
  protected readonly onBeginTransition = $hook({
17
19
  on: "react:transition:begin",
@@ -54,6 +56,14 @@ export class ReactAuth {
54
56
  return data.user;
55
57
  }
56
58
 
59
+ public can(action: string): boolean {
60
+ if (!this.user) {
61
+ return false;
62
+ }
63
+
64
+ return this.linkProvider.can(action);
65
+ }
66
+
57
67
  public async login(
58
68
  provider: string,
59
69
  options: {
@@ -50,7 +50,11 @@ const NestedView = (props: NestedViewProps) => {
50
50
  "react:transition:begin": async ({ previous, state }) => {
51
51
  // --------- Animations Begin ---------
52
52
  const layer = previous.layers[index];
53
- if (`${state.url.pathname}/`.startsWith(`${layer?.path}/`)) {
53
+ if (!layer) {
54
+ return;
55
+ }
56
+
57
+ if (`${state.url.pathname}/`.startsWith(`${layer.path}/`)) {
54
58
  return;
55
59
  }
56
60
 
@@ -4,20 +4,24 @@ export default function NotFoundPage(props: { style?: CSSProperties }) {
4
4
  return (
5
5
  <div
6
6
  style={{
7
- height: "100vh",
7
+ width: "100%",
8
+ minHeight: "90vh",
9
+ boxSizing: "border-box",
8
10
  display: "flex",
9
11
  flexDirection: "column",
10
12
  justifyContent: "center",
11
13
  alignItems: "center",
12
14
  textAlign: "center",
13
- fontFamily: "sans-serif",
14
- padding: "1rem",
15
+ fontFamily:
16
+ 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
17
+ padding: "2rem",
15
18
  ...props.style,
16
19
  }}
17
20
  >
18
- <h1 style={{ fontSize: "1rem", marginBottom: "0.5rem" }}>
19
- 404 - This page does not exist
20
- </h1>
21
+ <div style={{ fontSize: "6rem", fontWeight: 200, lineHeight: 1 }}>404</div>
22
+ <div style={{ fontSize: "0.875rem", marginTop: "1rem", opacity: 0.6 }}>
23
+ Page not found
24
+ </div>
21
25
  </div>
22
26
  );
23
27
  }
@@ -77,6 +77,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
77
77
  }
78
78
  }
79
79
 
80
+ state.name = route?.page.name;
80
81
  state.query = query;
81
82
  state.params = params ?? {};
82
83
 
@@ -694,6 +694,9 @@ export interface ReactRouterState {
694
694
  * Optional meta information associated with the current page.
695
695
  */
696
696
  meta: Record<string, any>;
697
+
698
+ //
699
+ name?: string;
697
700
  }
698
701
 
699
702
  export interface RouterStackItem {