@pyreon/head 0.5.0 → 0.5.1

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 @@
1
+ {"version":3,"file":"ssr.d.ts","names":[],"sources":["../../src/context.ts","../../src/ssr.ts"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { VNode } from "@pyreon/core";
2
+
3
+ //#region src/ssr.d.ts
4
+ /**
5
+ * Render a Pyreon app to an HTML fragment + a serialized <head> string.
6
+ *
7
+ * The returned `head` string can be injected directly into your HTML template:
8
+ *
9
+ * @example
10
+ * const { html, head } = await renderWithHead(h(App, null))
11
+ * const page = `<!DOCTYPE html>
12
+ * <html>
13
+ * <head>
14
+ * <meta charset="UTF-8" />
15
+ * ${head}
16
+ * </head>
17
+ * <body><div id="app">${html}</div></body>
18
+ * </html>`
19
+ */
20
+ interface RenderWithHeadResult {
21
+ html: string;
22
+ head: string;
23
+ /** Attributes to set on the <html> element */
24
+ htmlAttrs: Record<string, string>;
25
+ /** Attributes to set on the <body> element */
26
+ bodyAttrs: Record<string, string>;
27
+ }
28
+ declare function renderWithHead(app: VNode): Promise<RenderWithHeadResult>;
29
+ //#endregion
30
+ export { RenderWithHeadResult, renderWithHead };
31
+ //# sourceMappingURL=ssr2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssr2.d.ts","names":[],"sources":["../../src/ssr.ts"],"mappings":";;;;;AAwBA;;;;;;;;;;;;AASA;;UATiB,oBAAA;EACf,IAAA;EACA,IAAA;EAOgD;EALhD,SAAA,EAAW,MAAA;EAK4C;EAHvD,SAAA,EAAW,MAAA;AAAA;AAAA,iBAGS,cAAA,CAAe,GAAA,EAAK,KAAA,GAAQ,OAAA,CAAQ,oBAAA"}
@@ -0,0 +1,206 @@
1
+ import { createContext, onMount, onUnmount, useContext } from "@pyreon/core";
2
+ import { effect } from "@pyreon/reactivity";
3
+
4
+ //#region src/context.ts
5
+
6
+ /**
7
+ * Sync the resolved head tags to the real DOM <head>.
8
+ * Uses incremental diffing: matches existing elements by key, patches attributes
9
+ * in-place, adds new elements, and removes stale ones.
10
+ * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.
11
+ * No-op on the server (typeof document === "undefined").
12
+ */
13
+ function patchExistingTag(found, tag, kept) {
14
+ kept.add(found.getAttribute(ATTR));
15
+ patchAttrs(found, tag.props);
16
+ const content = String(tag.children);
17
+ if (found.textContent !== content) found.textContent = content;
18
+ }
19
+ function createNewTag(tag) {
20
+ const el = document.createElement(tag.tag);
21
+ const key = tag.key;
22
+ el.setAttribute(ATTR, key);
23
+ for (const [k, v] of Object.entries(tag.props)) el.setAttribute(k, v);
24
+ if (tag.children) el.textContent = tag.children;
25
+ document.head.appendChild(el);
26
+ managedElements.set(key, el);
27
+ }
28
+ function syncDom(ctx) {
29
+ if (typeof document === "undefined") return;
30
+ const tags = ctx.resolve();
31
+ const titleTemplate = ctx.resolveTitleTemplate();
32
+ let needsSeed = managedElements.size === 0;
33
+ if (!needsSeed) {
34
+ const sample = managedElements.values().next().value;
35
+ if (sample && !sample.isConnected) {
36
+ managedElements.clear();
37
+ needsSeed = true;
38
+ }
39
+ }
40
+ if (needsSeed) {
41
+ const existing = document.head.querySelectorAll(`[${ATTR}]`);
42
+ for (const el of existing) managedElements.set(el.getAttribute(ATTR), el);
43
+ }
44
+ const kept = /* @__PURE__ */new Set();
45
+ for (const tag of tags) {
46
+ if (tag.tag === "title") {
47
+ document.title = applyTitleTemplate(String(tag.children), titleTemplate);
48
+ continue;
49
+ }
50
+ const key = tag.key;
51
+ const found = managedElements.get(key);
52
+ if (found && found.tagName.toLowerCase() === tag.tag) patchExistingTag(found, tag, kept);else {
53
+ if (found) {
54
+ found.remove();
55
+ managedElements.delete(key);
56
+ }
57
+ createNewTag(tag);
58
+ kept.add(key);
59
+ }
60
+ }
61
+ for (const [key, el] of managedElements) if (!kept.has(key)) {
62
+ el.remove();
63
+ managedElements.delete(key);
64
+ }
65
+ syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs());
66
+ syncElementAttrs(document.body, ctx.resolveBodyAttrs());
67
+ }
68
+ /** Patch an element's attributes to match the desired props. */
69
+ function patchAttrs(el, props) {
70
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
71
+ const attr = el.attributes[i];
72
+ if (!attr || attr.name === ATTR) continue;
73
+ if (!(attr.name in props)) el.removeAttribute(attr.name);
74
+ }
75
+ for (const [k, v] of Object.entries(props)) if (el.getAttribute(k) !== v) el.setAttribute(k, v);
76
+ }
77
+ function applyTitleTemplate(title, template) {
78
+ if (!template) return title;
79
+ if (typeof template === "function") return template(title);
80
+ return template.replace(/%s/g, title);
81
+ }
82
+ /** Sync pyreon-managed attributes on <html> or <body>. */
83
+ function syncElementAttrs(el, attrs) {
84
+ const managed = el.getAttribute(`${ATTR}-attrs`);
85
+ if (managed) {
86
+ for (const name of managed.split(",")) if (name && !(name in attrs)) el.removeAttribute(name);
87
+ }
88
+ const keys = [];
89
+ for (const [k, v] of Object.entries(attrs)) {
90
+ keys.push(k);
91
+ if (el.getAttribute(k) !== v) el.setAttribute(k, v);
92
+ }
93
+ if (keys.length > 0) el.setAttribute(`${ATTR}-attrs`, keys.join(","));else if (managed) el.removeAttribute(`${ATTR}-attrs`);
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/use-head.ts
98
+ function buildEntry(o) {
99
+ const tags = [];
100
+ if (o.title != null) tags.push({
101
+ tag: "title",
102
+ key: "title",
103
+ children: o.title
104
+ });
105
+ o.meta?.forEach((m, i) => {
106
+ tags.push({
107
+ tag: "meta",
108
+ key: m.name ?? m.property ?? `meta-${i}`,
109
+ props: m
110
+ });
111
+ });
112
+ o.link?.forEach((l, i) => {
113
+ tags.push({
114
+ tag: "link",
115
+ key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
116
+ props: l
117
+ });
118
+ });
119
+ o.script?.forEach((s, i) => {
120
+ const {
121
+ children,
122
+ ...rest
123
+ } = s;
124
+ tags.push({
125
+ tag: "script",
126
+ key: s.src ?? `script-${i}`,
127
+ props: rest,
128
+ ...(children != null ? {
129
+ children
130
+ } : {})
131
+ });
132
+ });
133
+ o.style?.forEach((s, i) => {
134
+ const {
135
+ children,
136
+ ...rest
137
+ } = s;
138
+ tags.push({
139
+ tag: "style",
140
+ key: `style-${i}`,
141
+ props: rest,
142
+ children
143
+ });
144
+ });
145
+ o.noscript?.forEach((ns, i) => {
146
+ tags.push({
147
+ tag: "noscript",
148
+ key: `noscript-${i}`,
149
+ children: ns.children
150
+ });
151
+ });
152
+ if (o.jsonLd) tags.push({
153
+ tag: "script",
154
+ key: "jsonld",
155
+ props: {
156
+ type: "application/ld+json"
157
+ },
158
+ children: JSON.stringify(o.jsonLd)
159
+ });
160
+ if (o.base) tags.push({
161
+ tag: "base",
162
+ key: "base",
163
+ props: o.base
164
+ });
165
+ return {
166
+ tags,
167
+ titleTemplate: o.titleTemplate,
168
+ htmlAttrs: o.htmlAttrs,
169
+ bodyAttrs: o.bodyAttrs
170
+ };
171
+ }
172
+ /**
173
+ * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
174
+ * for the current component.
175
+ *
176
+ * Accepts a static object or a reactive getter:
177
+ * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
178
+ * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
179
+ *
180
+ * Tags are deduplicated by key — innermost component wins.
181
+ * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
182
+ */
183
+ function useHead(input) {
184
+ const ctx = useContext(HeadContext);
185
+ if (!ctx) return;
186
+ const id = Symbol();
187
+ if (typeof input === "function") {
188
+ if (typeof document !== "undefined") effect(() => {
189
+ ctx.add(id, buildEntry(input()));
190
+ syncDom(ctx);
191
+ });else ctx.add(id, buildEntry(input()));
192
+ } else {
193
+ ctx.add(id, buildEntry(input));
194
+ onMount(() => {
195
+ syncDom(ctx);
196
+ });
197
+ }
198
+ onUnmount(() => {
199
+ ctx.remove(id);
200
+ syncDom(ctx);
201
+ });
202
+ }
203
+
204
+ //#endregion
205
+ export { useHead };
206
+ //# sourceMappingURL=use-head.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-head.d.ts","names":[],"sources":["../../src/context.ts","../../src/dom.ts","../../src/use-head.ts"],"mappings":";;;;;;;;;;;;ACcA,SAAS,gBAAA,CACP,KAAA,EACA,GAAA,EACA,IAAA,EACM;EACN,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,YAAA,CAAa,IAAA,CAAK,CAAW;EAC5C,UAAA,CAAW,KAAA,EAAO,GAAA,CAAI,KAAA,CAAgC;EACtD,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,QAAA,CAAS;EACpC,IAAI,KAAA,CAAM,WAAA,KAAgB,OAAA,EAAS,KAAA,CAAM,WAAA,GAAc,OAAA;;AAGzD,SAAS,YAAA,CAAa,GAAA,EAKb;EACP,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAAc,GAAA,CAAI,GAAA,CAAI;EAC1C,MAAM,GAAA,GAAM,GAAA,CAAI,GAAA;EAChB,EAAA,CAAG,YAAA,CAAa,IAAA,EAAM,GAAA,CAAI;EAC1B,KAAK,MAAM,CAAC,CAAA,EAAG,CAAA,CAAA,IAAM,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAgC,EACtE,EAAA,CAAG,YAAA,CAAa,CAAA,EAAG,CAAA,CAAE;EAEvB,IAAI,GAAA,CAAI,QAAA,EAAU,EAAA,CAAG,WAAA,GAAc,GAAA,CAAI,QAAA;EACvC,QAAA,CAAS,IAAA,CAAK,WAAA,CAAY,EAAA,CAAG;EAC7B,eAAA,CAAgB,GAAA,CAAI,GAAA,EAAK,EAAA,CAAG;;AAG9B,SAAgB,OAAA,CAAQ,GAAA,EAA6B;EACnD,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa;EAErC,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAA,CAAS;EAC1B,MAAM,aAAA,GAAgB,GAAA,CAAI,oBAAA,CAAA,CAAsB;EAGhD,IAAI,SAAA,GAAY,eAAA,CAAgB,IAAA,KAAS,CAAA;EACzC,IAAI,CAAC,SAAA,EAAW;IAEd,MAAM,MAAA,GAAS,eAAA,CAAgB,MAAA,CAAA,CAAQ,CAAC,IAAA,CAAA,CAAM,CAAC,KAAA;IAC/C,IAAI,MAAA,IAAU,CAAC,MAAA,CAAO,WAAA,EAAa;MACjC,eAAA,CAAgB,KAAA,CAAA,CAAO;MACvB,SAAA,GAAY,IAAA;;;EAGhB,IAAI,SAAA,EAAW;IACb,MAAM,QAAA,GAAW,QAAA,CAAS,IAAA,CAAK,gBAAA,CAAiB,IAAI,IAAA,GAAK,CAAG;IAC5D,KAAK,MAAM,EAAA,IAAM,QAAA,EACf,eAAA,CAAgB,GAAA,CAAI,EAAA,CAAG,YAAA,CAAa,IAAA,CAAK,EAAY,EAAA,CAAG;;EAI5D,MAAM,IAAA,GAAA,eAAO,IAAI,GAAA,CAAA,CAAa;EAE9B,KAAK,MAAM,GAAA,IAAO,IAAA,EAAM;IACtB,IAAI,GAAA,CAAI,GAAA,KAAQ,OAAA,EAAS;MACvB,QAAA,CAAS,KAAA,GAAQ,kBAAA,CAAmB,MAAA,CAAO,GAAA,CAAI,QAAA,CAAS,EAAE,aAAA,CAAc;MACxE;;IAGF,MAAM,GAAA,GAAM,GAAA,CAAI,GAAA;IAChB,MAAM,KAAA,GAAQ,eAAA,CAAgB,GAAA,CAAI,GAAA,CAAI;IAEtC,IAAI,KAAA,IAAS,KAAA,CAAM,OAAA,CAAQ,WAAA,CAAA,CAAa,KAAK,GAAA,CAAI,GAAA,EAC/C,gBAAA,CAAiB,KAAA,EAAO,GAAA,EAA6D,IAAA,CAAK,CAAA,KACrF;MACL,IAAI,KAAA,EAAO;QACT,KAAA,CAAM,MAAA,CAAA,CAAQ;QACd,eAAA,CAAgB,MAAA,CAAO,GAAA,CAAI;;MAE7B,YAAA,CACE,GAAA,CACD;MACD,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI;;;EAKjB,KAAK,MAAM,CAAC,GAAA,EAAK,EAAA,CAAA,IAAO,eAAA,EACtB,IAAI,CAAC,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,EAAE;IAClB,EAAA,CAAG,MAAA,CAAA,CAAQ;IACX,eAAA,CAAgB,MAAA,CAAO,GAAA,CAAI;;EAI/B,gBAAA,CAAiB,QAAA,CAAS,eAAA,EAAiB,GAAA,CAAI,gBAAA,CAAA,CAAkB,CAAC;EAClE,gBAAA,CAAiB,QAAA,CAAS,IAAA,EAAM,GAAA,CAAI,gBAAA,CAAA,CAAkB,CAAC;;;AAIzD,SAAS,UAAA,CAAW,EAAA,EAAa,KAAA,EAAqC;EACpE,KAAK,IAAI,CAAA,GAAI,EAAA,CAAG,UAAA,CAAW,MAAA,GAAS,CAAA,EAAG,CAAA,IAAK,CAAA,EAAG,CAAA,EAAA,EAAK;IAClD,MAAM,IAAA,GAAO,EAAA,CAAG,UAAA,CAAW,CAAA,CAAA;IAC3B,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,IAAA,KAAS,IAAA,EAAM;IACjC,IAAI,EAAE,IAAA,CAAK,IAAA,IAAQ,KAAA,CAAA,EAAQ,EAAA,CAAG,eAAA,CAAgB,IAAA,CAAK,IAAA,CAAK;;EAE1D,KAAK,MAAM,CAAC,CAAA,EAAG,CAAA,CAAA,IAAM,MAAA,CAAO,OAAA,CAAQ,KAAA,CAAM,EACxC,IAAI,EAAA,CAAG,YAAA,CAAa,CAAA,CAAE,KAAK,CAAA,EAAG,EAAA,CAAG,YAAA,CAAa,CAAA,EAAG,CAAA,CAAE;;AAIvD,SAAS,kBAAA,CACP,KAAA,EACA,QAAA,EACQ;EACR,IAAI,CAAC,QAAA,EAAU,OAAO,KAAA;EACtB,IAAI,OAAO,QAAA,KAAa,UAAA,EAAY,OAAO,QAAA,CAAS,KAAA,CAAM;EAC1D,OAAO,QAAA,CAAS,OAAA,CAAQ,KAAA,EAAO,KAAA,CAAM;;;AAIvC,SAAS,gBAAA,CAAiB,EAAA,EAAa,KAAA,EAAqC;EAE1E,MAAM,OAAA,GAAU,EAAA,CAAG,YAAA,CAAa,GAAG,IAAA,QAAK,CAAQ;EAChD,IAAI,OAAA,EACF;SAAK,MAAM,IAAA,IAAQ,OAAA,CAAQ,KAAA,CAAM,GAAA,CAAI,EACnC,IAAI,IAAA,IAAQ,EAAE,IAAA,IAAQ,KAAA,CAAA,EAAQ,EAAA,CAAG,eAAA,CAAgB,IAAA,CAAK;;EAG1D,MAAM,IAAA,GAAiB,EAAE;EACzB,KAAK,MAAM,CAAC,CAAA,EAAG,CAAA,CAAA,IAAM,MAAA,CAAO,OAAA,CAAQ,KAAA,CAAM,EAAE;IAC1C,IAAA,CAAK,IAAA,CAAK,CAAA,CAAE;IACZ,IAAI,EAAA,CAAG,YAAA,CAAa,CAAA,CAAE,KAAK,CAAA,EAAG,EAAA,CAAG,YAAA,CAAa,CAAA,EAAG,CAAA,CAAE;;EAErD,IAAI,IAAA,CAAK,MAAA,GAAS,CAAA,EAChB,EAAA,CAAG,YAAA,CAAa,GAAG,IAAA,QAAK,EAAS,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,SACvC,OAAA,EACT,EAAA,CAAG,eAAA,CAAgB,GAAG,IAAA,QAAK,CAAQ;;;;;ACtIvC,SAAS,UAAA,CAAW,CAAA,EAA4B;EAC9C,MAAM,IAAA,GAAkB,EAAE;EAC1B,IAAI,CAAA,CAAE,KAAA,IAAS,IAAA,EAAM,IAAA,CAAK,IAAA,CAAK;IAAE,GAAA,EAAK,OAAA;IAAS,GAAA,EAAK,OAAA;IAAS,QAAA,EAAU,CAAA,CAAE;GAAO,CAAC;EACjF,CAAA,CAAE,IAAA,EAAM,OAAA,CAAA,CAAS,CAAA,EAAG,CAAA,KAAM;IACxB,IAAA,CAAK,IAAA,CAAK;MACR,GAAA,EAAK,MAAA;MACL,GAAA,EAAK,CAAA,CAAE,IAAA,IAAQ,CAAA,CAAE,QAAA,IAAY,QAAQ,CAAA,EAAA;MACrC,KAAA,EAAO;KACR,CAAC;IACF;EACF,CAAA,CAAE,IAAA,EAAM,OAAA,CAAA,CAAS,CAAA,EAAG,CAAA,KAAM;IACxB,IAAA,CAAK,IAAA,CAAK;MACR,GAAA,EAAK,MAAA;MACL,GAAA,EAAK,CAAA,CAAE,IAAA,GAAO,QAAQ,CAAA,CAAE,GAAA,IAAO,EAAA,IAAM,CAAA,CAAE,IAAA,EAAA,GAAS,CAAA,CAAE,GAAA,GAAM,QAAQ,CAAA,CAAE,GAAA,EAAA,GAAQ,QAAQ,CAAA,EAAA;MAClF,KAAA,EAAO;KACR,CAAC;IACF;EACF,CAAA,CAAE,MAAA,EAAQ,OAAA,CAAA,CAAS,CAAA,EAAG,CAAA,KAAM;IAC1B,MAAM;MAAE,QAAA;MAAU,GAAG;IAAA,CAAA,GAAS,CAAA;IAC9B,IAAA,CAAK,IAAA,CAAK;MACR,GAAA,EAAK,QAAA;MACL,GAAA,EAAK,CAAA,CAAE,GAAA,IAAO,UAAU,CAAA,EAAA;MACxB,KAAA,EAAO,IAAA;MACP,IAAI,QAAA,IAAY,IAAA,GAAO;QAAE;MAAA,CAAU,GAAG,CAAA,CAAE;KACzC,CAAC;IACF;EACF,CAAA,CAAE,KAAA,EAAO,OAAA,CAAA,CAAS,CAAA,EAAG,CAAA,KAAM;IACzB,MAAM;MAAE,QAAA;MAAU,GAAG;IAAA,CAAA,GAAS,CAAA;IAC9B,IAAA,CAAK,IAAA,CAAK;MACR,GAAA,EAAK,OAAA;MACL,GAAA,EAAK,SAAS,CAAA,EAAA;MACd,KAAA,EAAO,IAAA;MACP;KACD,CAAC;IACF;EACF,CAAA,CAAE,QAAA,EAAU,OAAA,CAAA,CAAS,EAAA,EAAI,CAAA,KAAM;IAC7B,IAAA,CAAK,IAAA,CAAK;MAAE,GAAA,EAAK,UAAA;MAAY,GAAA,EAAK,YAAY,CAAA,EAAA;MAAK,QAAA,EAAU,EAAA,CAAG;KAAU,CAAC;IAC3E;EACF,IAAI,CAAA,CAAE,MAAA,EACJ,IAAA,CAAK,IAAA,CAAK;IACR,GAAA,EAAK,QAAA;IACL,GAAA,EAAK,QAAA;IACL,KAAA,EAAO;MAAE,IAAA,EAAM;IAAA,CAAuB;IACtC,QAAA,EAAU,IAAA,CAAK,SAAA,CAAU,CAAA,CAAE,MAAA;GAC5B,CAAC;EAEJ,IAAI,CAAA,CAAE,IAAA,EAAM,IAAA,CAAK,IAAA,CAAK;IAAE,GAAA,EAAK,MAAA;IAAQ,GAAA,EAAK,MAAA;IAAQ,KAAA,EAAO,CAAA,CAAE;GAAM,CAAC;EAClE,OAAO;IACL,IAAA;IACA,aAAA,EAAe,CAAA,CAAE,aAAA;IACjB,SAAA,EAAW,CAAA,CAAE,SAAA;IACb,SAAA,EAAW,CAAA,CAAE;GACd;;;;;;;;;;;;;AAcH,SAAgB,OAAA,CAAQ,KAAA,EAAkD;EACxE,MAAM,GAAA,GAAM,UAAA,CAAW,WAAA,CAAY;EACnC,IAAI,CAAC,GAAA,EAAK;EAEV,MAAM,EAAA,GAAK,MAAA,CAAA,CAAQ;EAEnB,IAAI,OAAO,KAAA,KAAU,UAAA;IACnB,IAAI,OAAO,QAAA,KAAa,WAAA,EAEtB,MAAA,CAAA,MAAa;MACX,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,UAAA,CAAW,KAAA,CAAA,CAAO,CAAC,CAAC;MAChC,OAAA,CAAQ,GAAA,CAAI;MACZ,CAAA,KAGF,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,UAAA,CAAW,KAAA,CAAA,CAAO,CAAC,CAAC;EAAA,OAE7B;IACL,GAAA,CAAI,GAAA,CAAI,EAAA,EAAI,UAAA,CAAW,KAAA,CAAM,CAAC;IAC9B,OAAA,CAAA,MAAc;MACZ,OAAA,CAAQ,GAAA,CAAI;MAEZ;;EAGJ,SAAA,CAAA,MAAgB;IACd,GAAA,CAAI,MAAA,CAAO,EAAA,CAAG;IACd,OAAA,CAAQ,GAAA,CAAI;IACZ"}
@@ -0,0 +1,46 @@
1
+ //#region src/context.d.ts
2
+ interface UseHeadInput {
3
+ title?: string;
4
+ /**
5
+ * Title template — use `%s` as a placeholder for the page title.
6
+ * Applied to the resolved title after deduplication.
7
+ * @example useHead({ titleTemplate: "%s | My App" })
8
+ */
9
+ titleTemplate?: string | ((title: string) => string);
10
+ meta?: Record<string, string>[];
11
+ link?: Record<string, string>[];
12
+ script?: ({
13
+ src?: string;
14
+ children?: string;
15
+ } & Record<string, string | undefined>)[];
16
+ style?: ({
17
+ children: string;
18
+ } & Record<string, string | undefined>)[];
19
+ noscript?: {
20
+ children: string;
21
+ }[];
22
+ /** Convenience: emits a <script type="application/ld+json"> tag with JSON.stringify'd content */
23
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
24
+ base?: Record<string, string>;
25
+ /** Attributes to set on the <html> element (e.g. { lang: "en", dir: "ltr" }) */
26
+ htmlAttrs?: Record<string, string>;
27
+ /** Attributes to set on the <body> element (e.g. { class: "dark" }) */
28
+ bodyAttrs?: Record<string, string>;
29
+ }
30
+ //#endregion
31
+ //#region src/use-head.d.ts
32
+ /**
33
+ * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
34
+ * for the current component.
35
+ *
36
+ * Accepts a static object or a reactive getter:
37
+ * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
38
+ * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
39
+ *
40
+ * Tags are deduplicated by key — innermost component wins.
41
+ * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
42
+ */
43
+ declare function useHead(input: UseHeadInput | (() => UseHeadInput)): void;
44
+ //#endregion
45
+ export { useHead };
46
+ //# sourceMappingURL=use-head2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-head2.d.ts","names":[],"sources":["../../src/context.ts","../../src/use-head.ts"],"mappings":";UAmBiB,YAAA;EACf,KAAA;EAkBkB;;;;;EAZlB,aAAA,cAA2B,KAAA;EAC3B,IAAA,GAAO,MAAA;EACP,IAAA,GAAO,MAAA;EACP,MAAA;IAAY,GAAA;IAAc,QAAA;EAAA,IAAsB,MAAA;EAChD,KAAA;IAAW,QAAA;EAAA,IAAqB,MAAA;EAChC,QAAA;IAAa,QAAA;EAAA;EAEJ;EAAT,MAAA,GAAS,MAAA,oBAA0B,MAAA;EACnC,IAAA,GAAO,MAAA;EAAA;EAEP,SAAA,GAAY,MAAA;EAAA;EAEZ,SAAA,GAAY,MAAA;AAAA;;;;;AAnBd;;;;;;;;;iBCqDgB,OAAA,CAAQ,KAAA,EAAO,YAAA,UAAsB,YAAA"}
@@ -0,0 +1,204 @@
1
+ import { createContext, onMount, onUnmount, useContext } from "@pyreon/core";
2
+ import { effect } from "@pyreon/reactivity";
3
+
4
+ //#region src/context.ts
5
+ const HeadContext = createContext(null);
6
+
7
+ //#endregion
8
+ //#region src/dom.ts
9
+ const ATTR = "data-pyreon-head";
10
+ /** Tracks managed elements by key — avoids querySelectorAll on every sync */
11
+ const managedElements = /* @__PURE__ */ new Map();
12
+ /**
13
+ * Sync the resolved head tags to the real DOM <head>.
14
+ * Uses incremental diffing: matches existing elements by key, patches attributes
15
+ * in-place, adds new elements, and removes stale ones.
16
+ * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.
17
+ * No-op on the server (typeof document === "undefined").
18
+ */
19
+ function patchExistingTag(found, tag, kept) {
20
+ kept.add(found.getAttribute(ATTR));
21
+ patchAttrs(found, tag.props);
22
+ const content = String(tag.children);
23
+ if (found.textContent !== content) found.textContent = content;
24
+ }
25
+ function createNewTag(tag) {
26
+ const el = document.createElement(tag.tag);
27
+ const key = tag.key;
28
+ el.setAttribute(ATTR, key);
29
+ for (const [k, v] of Object.entries(tag.props)) el.setAttribute(k, v);
30
+ if (tag.children) el.textContent = tag.children;
31
+ document.head.appendChild(el);
32
+ managedElements.set(key, el);
33
+ }
34
+ function syncDom(ctx) {
35
+ if (typeof document === "undefined") return;
36
+ const tags = ctx.resolve();
37
+ const titleTemplate = ctx.resolveTitleTemplate();
38
+ let needsSeed = managedElements.size === 0;
39
+ if (!needsSeed) {
40
+ const sample = managedElements.values().next().value;
41
+ if (sample && !sample.isConnected) {
42
+ managedElements.clear();
43
+ needsSeed = true;
44
+ }
45
+ }
46
+ if (needsSeed) {
47
+ const existing = document.head.querySelectorAll(`[${ATTR}]`);
48
+ for (const el of existing) managedElements.set(el.getAttribute(ATTR), el);
49
+ }
50
+ const kept = /* @__PURE__ */ new Set();
51
+ for (const tag of tags) {
52
+ if (tag.tag === "title") {
53
+ document.title = applyTitleTemplate(String(tag.children), titleTemplate);
54
+ continue;
55
+ }
56
+ const key = tag.key;
57
+ const found = managedElements.get(key);
58
+ if (found && found.tagName.toLowerCase() === tag.tag) patchExistingTag(found, tag, kept);
59
+ else {
60
+ if (found) {
61
+ found.remove();
62
+ managedElements.delete(key);
63
+ }
64
+ createNewTag(tag);
65
+ kept.add(key);
66
+ }
67
+ }
68
+ for (const [key, el] of managedElements) if (!kept.has(key)) {
69
+ el.remove();
70
+ managedElements.delete(key);
71
+ }
72
+ syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs());
73
+ syncElementAttrs(document.body, ctx.resolveBodyAttrs());
74
+ }
75
+ /** Patch an element's attributes to match the desired props. */
76
+ function patchAttrs(el, props) {
77
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
78
+ const attr = el.attributes[i];
79
+ if (!attr || attr.name === ATTR) continue;
80
+ if (!(attr.name in props)) el.removeAttribute(attr.name);
81
+ }
82
+ for (const [k, v] of Object.entries(props)) if (el.getAttribute(k) !== v) el.setAttribute(k, v);
83
+ }
84
+ function applyTitleTemplate(title, template) {
85
+ if (!template) return title;
86
+ if (typeof template === "function") return template(title);
87
+ return template.replace(/%s/g, title);
88
+ }
89
+ /** Sync pyreon-managed attributes on <html> or <body>. */
90
+ function syncElementAttrs(el, attrs) {
91
+ const managed = el.getAttribute(`${ATTR}-attrs`);
92
+ if (managed) {
93
+ for (const name of managed.split(",")) if (name && !(name in attrs)) el.removeAttribute(name);
94
+ }
95
+ const keys = [];
96
+ for (const [k, v] of Object.entries(attrs)) {
97
+ keys.push(k);
98
+ if (el.getAttribute(k) !== v) el.setAttribute(k, v);
99
+ }
100
+ if (keys.length > 0) el.setAttribute(`${ATTR}-attrs`, keys.join(","));
101
+ else if (managed) el.removeAttribute(`${ATTR}-attrs`);
102
+ }
103
+
104
+ //#endregion
105
+ //#region src/use-head.ts
106
+ function buildEntry(o) {
107
+ const tags = [];
108
+ if (o.title != null) tags.push({
109
+ tag: "title",
110
+ key: "title",
111
+ children: o.title
112
+ });
113
+ o.meta?.forEach((m, i) => {
114
+ tags.push({
115
+ tag: "meta",
116
+ key: m.name ?? m.property ?? `meta-${i}`,
117
+ props: m
118
+ });
119
+ });
120
+ o.link?.forEach((l, i) => {
121
+ tags.push({
122
+ tag: "link",
123
+ key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
124
+ props: l
125
+ });
126
+ });
127
+ o.script?.forEach((s, i) => {
128
+ const { children, ...rest } = s;
129
+ tags.push({
130
+ tag: "script",
131
+ key: s.src ?? `script-${i}`,
132
+ props: rest,
133
+ ...children != null ? { children } : {}
134
+ });
135
+ });
136
+ o.style?.forEach((s, i) => {
137
+ const { children, ...rest } = s;
138
+ tags.push({
139
+ tag: "style",
140
+ key: `style-${i}`,
141
+ props: rest,
142
+ children
143
+ });
144
+ });
145
+ o.noscript?.forEach((ns, i) => {
146
+ tags.push({
147
+ tag: "noscript",
148
+ key: `noscript-${i}`,
149
+ children: ns.children
150
+ });
151
+ });
152
+ if (o.jsonLd) tags.push({
153
+ tag: "script",
154
+ key: "jsonld",
155
+ props: { type: "application/ld+json" },
156
+ children: JSON.stringify(o.jsonLd)
157
+ });
158
+ if (o.base) tags.push({
159
+ tag: "base",
160
+ key: "base",
161
+ props: o.base
162
+ });
163
+ return {
164
+ tags,
165
+ titleTemplate: o.titleTemplate,
166
+ htmlAttrs: o.htmlAttrs,
167
+ bodyAttrs: o.bodyAttrs
168
+ };
169
+ }
170
+ /**
171
+ * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
172
+ * for the current component.
173
+ *
174
+ * Accepts a static object or a reactive getter:
175
+ * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
176
+ * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
177
+ *
178
+ * Tags are deduplicated by key — innermost component wins.
179
+ * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
180
+ */
181
+ function useHead(input) {
182
+ const ctx = useContext(HeadContext);
183
+ if (!ctx) return;
184
+ const id = Symbol();
185
+ if (typeof input === "function") if (typeof document !== "undefined") effect(() => {
186
+ ctx.add(id, buildEntry(input()));
187
+ syncDom(ctx);
188
+ });
189
+ else ctx.add(id, buildEntry(input()));
190
+ else {
191
+ ctx.add(id, buildEntry(input));
192
+ onMount(() => {
193
+ syncDom(ctx);
194
+ });
195
+ }
196
+ onUnmount(() => {
197
+ ctx.remove(id);
198
+ syncDom(ctx);
199
+ });
200
+ }
201
+
202
+ //#endregion
203
+ export { useHead };
204
+ //# sourceMappingURL=use-head.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-head.js","names":[],"sources":["../src/context.ts","../src/dom.ts","../src/use-head.ts"],"sourcesContent":["import { createContext } from \"@pyreon/core\"\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface HeadTag {\n /** HTML tag name */\n tag: \"title\" | \"meta\" | \"link\" | \"script\" | \"style\" | \"base\" | \"noscript\"\n /**\n * Deduplication key. Tags with the same key replace each other;\n * innermost component (last added) wins.\n * Example: all components setting the page title use key \"title\".\n */\n key?: string\n /** HTML attributes for the tag */\n props?: Record<string, string>\n /** Text content — for <title>, <script>, <style>, <noscript> */\n children?: string\n}\n\nexport interface UseHeadInput {\n title?: string\n /**\n * Title template — use `%s` as a placeholder for the page title.\n * Applied to the resolved title after deduplication.\n * @example useHead({ titleTemplate: \"%s | My App\" })\n */\n titleTemplate?: string | ((title: string) => string)\n meta?: Record<string, string>[]\n link?: Record<string, string>[]\n script?: ({ src?: string; children?: string } & Record<string, string | undefined>)[]\n style?: ({ children: string } & Record<string, string | undefined>)[]\n noscript?: { children: string }[]\n /** Convenience: emits a <script type=\"application/ld+json\"> tag with JSON.stringify'd content */\n jsonLd?: Record<string, unknown> | Record<string, unknown>[]\n base?: Record<string, string>\n /** Attributes to set on the <html> element (e.g. { lang: \"en\", dir: \"ltr\" }) */\n htmlAttrs?: Record<string, string>\n /** Attributes to set on the <body> element (e.g. { class: \"dark\" }) */\n bodyAttrs?: Record<string, string>\n}\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport interface HeadEntry {\n tags: HeadTag[]\n titleTemplate?: string | ((title: string) => string) | undefined\n htmlAttrs?: Record<string, string> | undefined\n bodyAttrs?: Record<string, string> | undefined\n}\n\nexport interface HeadContextValue {\n add(id: symbol, entry: HeadEntry): void\n remove(id: symbol): void\n /** Returns deduplicated tags — last-added entry wins per key */\n resolve(): HeadTag[]\n /** Returns the merged titleTemplate (last-added wins) */\n resolveTitleTemplate(): (string | ((title: string) => string)) | undefined\n /** Returns merged htmlAttrs (later entries override earlier) */\n resolveHtmlAttrs(): Record<string, string>\n /** Returns merged bodyAttrs (later entries override earlier) */\n resolveBodyAttrs(): Record<string, string>\n}\n\nexport function createHeadContext(): HeadContextValue {\n const map = new Map<symbol, HeadEntry>()\n\n // ── Cached resolve ───────────────────────────────────────────────────────\n let dirty = true\n let cachedTags: HeadTag[] = []\n let cachedTitleTemplate: (string | ((title: string) => string)) | undefined\n let cachedHtmlAttrs: Record<string, string> = {}\n let cachedBodyAttrs: Record<string, string> = {}\n\n function rebuild(): void {\n if (!dirty) return\n dirty = false\n\n const keyed = new Map<string, HeadTag>()\n const unkeyed: HeadTag[] = []\n let titleTemplate: (string | ((title: string) => string)) | undefined\n const htmlAttrs: Record<string, string> = {}\n const bodyAttrs: Record<string, string> = {}\n\n for (const entry of map.values()) {\n for (const tag of entry.tags) {\n if (tag.key) keyed.set(tag.key, tag)\n else unkeyed.push(tag)\n }\n if (entry.titleTemplate !== undefined) titleTemplate = entry.titleTemplate\n if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs)\n if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs)\n }\n\n cachedTags = [...keyed.values(), ...unkeyed]\n cachedTitleTemplate = titleTemplate\n cachedHtmlAttrs = htmlAttrs\n cachedBodyAttrs = bodyAttrs\n }\n\n return {\n add(id, entry) {\n map.set(id, entry)\n dirty = true\n },\n remove(id) {\n map.delete(id)\n dirty = true\n },\n resolve() {\n rebuild()\n return cachedTags\n },\n resolveTitleTemplate() {\n rebuild()\n return cachedTitleTemplate\n },\n resolveHtmlAttrs() {\n rebuild()\n return cachedHtmlAttrs\n },\n resolveBodyAttrs() {\n rebuild()\n return cachedBodyAttrs\n },\n }\n}\n\nexport const HeadContext = createContext<HeadContextValue | null>(null)\n","import type { HeadContextValue } from \"./context\"\n\nconst ATTR = \"data-pyreon-head\"\n\n/** Tracks managed elements by key — avoids querySelectorAll on every sync */\nconst managedElements = new Map<string, Element>()\n\n/**\n * Sync the resolved head tags to the real DOM <head>.\n * Uses incremental diffing: matches existing elements by key, patches attributes\n * in-place, adds new elements, and removes stale ones.\n * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.\n * No-op on the server (typeof document === \"undefined\").\n */\nfunction patchExistingTag(\n found: Element,\n tag: { props: Record<string, unknown>; children: string },\n kept: Set<string>,\n): void {\n kept.add(found.getAttribute(ATTR) as string)\n patchAttrs(found, tag.props as Record<string, string>)\n const content = String(tag.children)\n if (found.textContent !== content) found.textContent = content\n}\n\nfunction createNewTag(tag: {\n tag: string\n props: Record<string, unknown>\n children: string\n key: unknown\n}): void {\n const el = document.createElement(tag.tag)\n const key = tag.key as string\n el.setAttribute(ATTR, key)\n for (const [k, v] of Object.entries(tag.props as Record<string, string>)) {\n el.setAttribute(k, v)\n }\n if (tag.children) el.textContent = tag.children\n document.head.appendChild(el)\n managedElements.set(key, el)\n}\n\nexport function syncDom(ctx: HeadContextValue): void {\n if (typeof document === \"undefined\") return\n\n const tags = ctx.resolve()\n const titleTemplate = ctx.resolveTitleTemplate()\n\n // Seed from DOM on first sync, or re-seed if DOM was reset (e.g. between tests)\n let needsSeed = managedElements.size === 0\n if (!needsSeed) {\n // Check if a tracked element is still in the DOM\n const sample = managedElements.values().next().value\n if (sample && !sample.isConnected) {\n managedElements.clear()\n needsSeed = true\n }\n }\n if (needsSeed) {\n const existing = document.head.querySelectorAll(`[${ATTR}]`)\n for (const el of existing) {\n managedElements.set(el.getAttribute(ATTR) as string, el)\n }\n }\n\n const kept = new Set<string>()\n\n for (const tag of tags) {\n if (tag.tag === \"title\") {\n document.title = applyTitleTemplate(String(tag.children), titleTemplate)\n continue\n }\n\n const key = tag.key as string\n const found = managedElements.get(key)\n\n if (found && found.tagName.toLowerCase() === tag.tag) {\n patchExistingTag(found, tag as { props: Record<string, unknown>; children: string }, kept)\n } else {\n if (found) {\n found.remove()\n managedElements.delete(key)\n }\n createNewTag(\n tag as { tag: string; props: Record<string, unknown>; children: string; key: unknown },\n )\n kept.add(key)\n }\n }\n\n // Remove stale elements\n for (const [key, el] of managedElements) {\n if (!kept.has(key)) {\n el.remove()\n managedElements.delete(key)\n }\n }\n\n syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs())\n syncElementAttrs(document.body, ctx.resolveBodyAttrs())\n}\n\n/** Patch an element's attributes to match the desired props. */\nfunction patchAttrs(el: Element, props: Record<string, string>): void {\n for (let i = el.attributes.length - 1; i >= 0; i--) {\n const attr = el.attributes[i]\n if (!attr || attr.name === ATTR) continue\n if (!(attr.name in props)) el.removeAttribute(attr.name)\n }\n for (const [k, v] of Object.entries(props)) {\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n}\n\nfunction applyTitleTemplate(\n title: string,\n template: string | ((t: string) => string) | undefined,\n): string {\n if (!template) return title\n if (typeof template === \"function\") return template(title)\n return template.replace(/%s/g, title)\n}\n\n/** Sync pyreon-managed attributes on <html> or <body>. */\nfunction syncElementAttrs(el: Element, attrs: Record<string, string>): void {\n // Remove previously managed attrs that are no longer present\n const managed = el.getAttribute(`${ATTR}-attrs`)\n if (managed) {\n for (const name of managed.split(\",\")) {\n if (name && !(name in attrs)) el.removeAttribute(name)\n }\n }\n const keys: string[] = []\n for (const [k, v] of Object.entries(attrs)) {\n keys.push(k)\n if (el.getAttribute(k) !== v) el.setAttribute(k, v)\n }\n if (keys.length > 0) {\n el.setAttribute(`${ATTR}-attrs`, keys.join(\",\"))\n } else if (managed) {\n el.removeAttribute(`${ATTR}-attrs`)\n }\n}\n","import { onMount, onUnmount, useContext } from \"@pyreon/core\"\nimport { effect } from \"@pyreon/reactivity\"\nimport type { HeadEntry, HeadTag, UseHeadInput } from \"./context\"\nimport { HeadContext } from \"./context\"\nimport { syncDom } from \"./dom\"\n\nfunction buildEntry(o: UseHeadInput): HeadEntry {\n const tags: HeadTag[] = []\n if (o.title != null) tags.push({ tag: \"title\", key: \"title\", children: o.title })\n o.meta?.forEach((m, i) => {\n tags.push({\n tag: \"meta\",\n key: m.name ?? m.property ?? `meta-${i}`,\n props: m,\n })\n })\n o.link?.forEach((l, i) => {\n tags.push({\n tag: \"link\",\n key: l.href ? `link-${l.rel || \"\"}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,\n props: l,\n })\n })\n o.script?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: \"script\",\n key: s.src ?? `script-${i}`,\n props: rest as Record<string, string>,\n ...(children != null ? { children } : {}),\n })\n })\n o.style?.forEach((s, i) => {\n const { children, ...rest } = s\n tags.push({\n tag: \"style\",\n key: `style-${i}`,\n props: rest as Record<string, string>,\n children,\n })\n })\n o.noscript?.forEach((ns, i) => {\n tags.push({ tag: \"noscript\", key: `noscript-${i}`, children: ns.children })\n })\n if (o.jsonLd) {\n tags.push({\n tag: \"script\",\n key: \"jsonld\",\n props: { type: \"application/ld+json\" },\n children: JSON.stringify(o.jsonLd),\n })\n }\n if (o.base) tags.push({ tag: \"base\", key: \"base\", props: o.base })\n return {\n tags,\n titleTemplate: o.titleTemplate,\n htmlAttrs: o.htmlAttrs,\n bodyAttrs: o.bodyAttrs,\n }\n}\n\n/**\n * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)\n * for the current component.\n *\n * Accepts a static object or a reactive getter:\n * useHead({ title: \"My Page\", meta: [{ name: \"description\", content: \"...\" }] })\n * useHead(() => ({ title: `${count()} items` })) // updates when signal changes\n *\n * Tags are deduplicated by key — innermost component wins.\n * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.\n */\nexport function useHead(input: UseHeadInput | (() => UseHeadInput)): void {\n const ctx = useContext(HeadContext)\n if (!ctx) return // no HeadProvider — silently no-op\n\n const id = Symbol()\n\n if (typeof input === \"function\") {\n if (typeof document !== \"undefined\") {\n // CSR: reactive — re-register whenever signals change\n effect(() => {\n ctx.add(id, buildEntry(input()))\n syncDom(ctx)\n })\n } else {\n // SSR: evaluate once synchronously (no effects on server)\n ctx.add(id, buildEntry(input()))\n }\n } else {\n ctx.add(id, buildEntry(input))\n onMount(() => {\n syncDom(ctx)\n return undefined\n })\n }\n\n onUnmount(() => {\n ctx.remove(id)\n syncDom(ctx)\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/head",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Head tag management for Pyreon — works in SSR and CSR",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "types": "./lib/types/index.d.ts"
31
31
  },
32
32
  "./provider": {
33
- "bun": "./src/provider.tsx",
33
+ "bun": "./src/provider.ts",
34
34
  "import": "./lib/provider.js",
35
35
  "types": "./lib/types/provider.d.ts"
36
36
  },
@@ -54,9 +54,9 @@
54
54
  "prepublishOnly": "bun run build"
55
55
  },
56
56
  "dependencies": {
57
- "@pyreon/core": "^0.5.0",
58
- "@pyreon/reactivity": "^0.5.0",
59
- "@pyreon/runtime-server": "^0.5.0"
57
+ "@pyreon/core": "^0.5.1",
58
+ "@pyreon/reactivity": "^0.5.1",
59
+ "@pyreon/runtime-server": "^0.5.1"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@happy-dom/global-registrator": "*",
File without changes