@pyreon/head 0.1.0

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.
@@ -0,0 +1,3 @@
1
+ import { GlobalRegistrator } from "@happy-dom/global-registrator"
2
+
3
+ GlobalRegistrator.register()
@@ -0,0 +1,104 @@
1
+ import { onMount, onUnmount, useContext } from "@pyreon/core"
2
+ import { effect } from "@pyreon/reactivity"
3
+ import type { HeadEntry, HeadTag, UseHeadInput } from "./context"
4
+ import { HeadContext } from "./context"
5
+ import { syncDom } from "./dom"
6
+
7
+ function buildEntry(o: UseHeadInput): HeadEntry {
8
+ const tags: HeadTag[] = []
9
+ if (o.title != null) tags.push({ tag: "title", key: "title", children: o.title })
10
+ o.meta?.forEach((m, i) => {
11
+ tags.push({
12
+ tag: "meta",
13
+ key: m.name ?? m.property ?? `meta-${i}`,
14
+ props: m,
15
+ })
16
+ })
17
+ o.link?.forEach((l, i) => {
18
+ tags.push({
19
+ tag: "link",
20
+ key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
21
+ props: l,
22
+ })
23
+ })
24
+ o.script?.forEach((s, i) => {
25
+ const { children, ...rest } = s
26
+ tags.push({
27
+ tag: "script",
28
+ key: s.src ?? `script-${i}`,
29
+ props: rest as Record<string, string>,
30
+ ...(children != null ? { children } : {}),
31
+ })
32
+ })
33
+ o.style?.forEach((s, i) => {
34
+ const { children, ...rest } = s
35
+ tags.push({
36
+ tag: "style",
37
+ key: `style-${i}`,
38
+ props: rest as Record<string, string>,
39
+ children,
40
+ })
41
+ })
42
+ o.noscript?.forEach((ns, i) => {
43
+ tags.push({ tag: "noscript", key: `noscript-${i}`, children: ns.children })
44
+ })
45
+ if (o.jsonLd) {
46
+ tags.push({
47
+ tag: "script",
48
+ key: "jsonld",
49
+ props: { type: "application/ld+json" },
50
+ children: JSON.stringify(o.jsonLd),
51
+ })
52
+ }
53
+ if (o.base) tags.push({ tag: "base", key: "base", props: o.base })
54
+ return {
55
+ tags,
56
+ titleTemplate: o.titleTemplate,
57
+ htmlAttrs: o.htmlAttrs,
58
+ bodyAttrs: o.bodyAttrs,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
64
+ * for the current component.
65
+ *
66
+ * Accepts a static object or a reactive getter:
67
+ * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
68
+ * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
69
+ *
70
+ * Tags are deduplicated by key — innermost component wins.
71
+ * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
72
+ */
73
+ export function useHead(input: UseHeadInput | (() => UseHeadInput)): void {
74
+ const ctx = useContext(HeadContext)
75
+ if (!ctx) return // no HeadProvider — silently no-op
76
+
77
+ const id = Symbol()
78
+
79
+ if (typeof input === "function") {
80
+ if (typeof document !== "undefined") {
81
+ // CSR: reactive — re-register whenever signals change
82
+ effect(() => {
83
+ ctx.add(id, buildEntry(input()))
84
+ syncDom(ctx)
85
+ })
86
+ } else {
87
+ // SSR: evaluate once synchronously (no effects on server)
88
+ ctx.add(id, buildEntry(input()))
89
+ }
90
+ } else {
91
+ ctx.add(id, buildEntry(input))
92
+ // onMount is a no-op in SSR; syncDom has its own typeof document guard
93
+ onMount(() => {
94
+ syncDom(ctx)
95
+ return undefined
96
+ })
97
+ }
98
+
99
+ onUnmount(() => {
100
+ ctx.remove(id)
101
+ // syncDom has its own typeof document guard — no need to check here
102
+ syncDom(ctx)
103
+ })
104
+ }