@pyreon/head 0.21.0 → 0.23.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.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @pyreon/head
2
2
 
3
- Manage document head tags (title, meta, link, style, script, JSON-LD) with Pyreon's reactivity system. Supports SSR via `renderWithHead`.
3
+ Reactive `<head>` tag management `useHead()` + `HeadProvider` + SSR `renderWithHead()`.
4
+
5
+ Register `<title>` / `<meta>` / `<link>` / `<script>` / `<style>` / `<noscript>` / `<base>` / JSON-LD / Speculation Rules entries from ANY component (static or signal-driven via a thunk). `<HeadProvider>` collects them on the client and syncs to the live `document.head`. `renderWithHead()` (subpath `/ssr`) collects them server-side and returns a serialized `head` string plus `htmlAttrs` / `bodyAttrs`. Innermost component wins per key; the inheritance contract makes `<HeadProvider>` mounted inside `renderWithHead()` compose without manual context plumbing.
4
6
 
5
7
  ## Install
6
8
 
@@ -8,58 +10,174 @@ Manage document head tags (title, meta, link, style, script, JSON-LD) with Pyreo
8
10
  bun add @pyreon/head
9
11
  ```
10
12
 
11
- ## Quick Start
13
+ For SSR you'll also need `@pyreon/runtime-server` (peer).
14
+
15
+ ## Quick start
12
16
 
13
17
  ```tsx
14
18
  import { HeadProvider, useHead } from '@pyreon/head'
19
+ import { renderWithHead } from '@pyreon/head/ssr'
20
+ import { mount } from '@pyreon/runtime-dom'
15
21
 
16
- const App = () => {
22
+ function ProfilePage() {
17
23
  useHead({
18
- title: 'My App',
24
+ title: 'My Profile',
19
25
  meta: [
20
- { name: 'description', content: 'A Pyreon application' },
21
- { property: 'og:title', content: 'My App' },
26
+ { name: 'description', content: 'User profile page' },
27
+ { property: 'og:title', content: 'My Profile' },
28
+ { property: 'og:image', content: 'https://example.com/og.jpg' },
22
29
  ],
23
- link: [{ rel: 'canonical', href: 'https://example.com' }],
30
+ link: [{ rel: 'canonical', href: 'https://example.com/profile' }],
24
31
  })
25
-
26
- return <div>Hello</div>
32
+ return <div>profile body</div>
27
33
  }
28
34
 
29
- // Wrap your app with HeadProvider
30
- const Root = () => (
35
+ // CSR
36
+ mount(
31
37
  <HeadProvider>
32
- <App />
33
- </HeadProvider>
38
+ <ProfilePage />
39
+ </HeadProvider>,
40
+ document.getElementById('app')!,
34
41
  )
42
+
43
+ // SSR — note the /ssr subpath
44
+ const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<ProfilePage />)
35
45
  ```
36
46
 
37
- ## SSR
47
+ ## Reactive head — thunk form
38
48
 
39
- Use `renderWithHead` to capture head tags during server-side rendering:
49
+ Pass a function so signal reads re-register on change:
40
50
 
41
51
  ```tsx
42
- import { renderWithHead } from '@pyreon/head'
52
+ function ReactiveTitle({ username }: { username: () => string }) {
53
+ useHead(() => ({
54
+ title: `${username()} — Profile`,
55
+ meta: [{ property: 'og:title', content: username() }],
56
+ }))
57
+ return null
58
+ }
59
+ ```
43
60
 
44
- const { html, head } = renderWithHead(<App />)
45
- // `head` contains the serialized <title>, <meta>, <link>, etc.
61
+ Static-object form registers ONCE; thunk form runs an effect that re-registers whenever a tracked signal changes.
62
+
63
+ ## All supported tags
64
+
65
+ ```ts
66
+ useHead({
67
+ title: 'Page',
68
+ titleTemplate: '%s | My App', // or: (title) => `${title} | My App`
69
+ meta: [{ name: 'description', content: '...' }],
70
+ link: [{ rel: 'stylesheet', href: '/app.css' }],
71
+ script: [{ src: '/analytics.js', defer: 'true' }],
72
+ style: [{ children: 'body { font-family: sans-serif }' }],
73
+ noscript: [{ children: 'Please enable JavaScript' }],
74
+ base: { href: 'https://example.com/' },
75
+ jsonLd: { '@context': 'https://schema.org', '@type': 'Article', headline: 'Hi' },
76
+ speculationRules: { prefetch: [{ urls: ['/next-page'], eagerness: 'moderate' }] },
77
+ htmlAttrs: { lang: 'en' },
78
+ bodyAttrs: { class: 'dark' },
79
+ })
46
80
  ```
47
81
 
48
- ## API
82
+ `jsonLd: {...}` is shorthand for a `<script type="application/ld+json">` tag with `JSON.stringify` applied. `speculationRules: {...}` is shorthand for `<script type="speculationrules">` — supported browsers prefetch/prerender at their own discretion; inert in non-supporting browsers.
83
+
84
+ ## Title templates
85
+
86
+ ```ts
87
+ useHead({ titleTemplate: '%s | My App' })
88
+ // Elsewhere:
89
+ useHead({ title: 'About' }) // → <title>About | My App</title>
90
+
91
+ // Function form for full control:
92
+ useHead({ titleTemplate: (t) => (t === 'Home' ? 'My App' : `${t} | My App`) })
93
+ ```
94
+
95
+ `%s` is the placeholder. Mismatched: `${...}` does NOT interpolate (the `pyreon/lint` rule `i18n-prefer-trans-for-rich-jsx` is a different concern).
96
+
97
+ ## Deduplication
98
+
99
+ Tags with the same `key` replace each other — innermost component wins.
100
+
101
+ | Tag | Key generation |
102
+ | -------------------- | ------------------------------------------------ |
103
+ | `title` | always `'title'` |
104
+ | `meta` | `name` → `property` → `http-equiv` → array index |
105
+ | `link` | `href + rel` → `rel` → index |
106
+ | `script` | `src` → index |
107
+ | `style` / `noscript` | unkeyed — always accumulated |
108
+
109
+ ## HeadProvider — context resolution
110
+
111
+ `HeadProvider` resolves its context in this order, first non-null wins:
112
+
113
+ 1. **`props.context`** — explicit context (for isolation / custom SSR pipelines)
114
+ 2. **An outer `HeadContext` already in scope** — inherited transparently
115
+ 3. **A fresh `HeadContext`** — root-level fallback (pure CSR)
116
+
117
+ This means `renderWithHead(<HeadProvider><App /></HeadProvider>)` composes correctly without manual plumbing — the outer ctx that `renderWithHead` pushed is inherited by the inner provider. A nested `<HeadProvider>` (e.g. inside another `<HeadProvider>`, or inside a meta-framework's `App` that mounts one unconditionally) **inherits, not isolates**. Pass `context={createHeadContext()}` explicitly when you genuinely want isolation (iframe / micro-frontend boundary).
118
+
119
+ ```tsx
120
+ // Default: nested HeadProvider inherits
121
+ <HeadProvider>
122
+ <HeadProvider> {/* same ctx as parent */}
123
+ <App />
124
+ </HeadProvider>
125
+ </HeadProvider>
126
+
127
+ // Opt-out: explicit isolated context
128
+ <HeadProvider>
129
+ <HeadProvider context={createHeadContext()}> {/* isolated ctx */}
130
+ <IsolatedSubApp />
131
+ </HeadProvider>
132
+ </HeadProvider>
133
+ ```
134
+
135
+ ## SSR
136
+
137
+ ```ts
138
+ import { renderWithHead } from '@pyreon/head/ssr'
139
+
140
+ const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
141
+
142
+ const page = `<!DOCTYPE html>
143
+ <html ${htmlAttrs}>
144
+ <head>
145
+ <meta charset="UTF-8" />
146
+ ${head}
147
+ </head>
148
+ <body ${bodyAttrs}>
149
+ <div id="app">${html}</div>
150
+ </body>
151
+ </html>`
152
+ ```
153
+
154
+ `renderWithHead` (subpath `@pyreon/head/ssr`) creates its own `HeadContext`, runs `renderToString` from `@pyreon/runtime-server`, then serializes the resolved tags. `htmlAttrs` / `bodyAttrs` are space-prefixed strings ready to splice into the opening tags.
155
+
156
+ **XSS hardening**: inline script/style/noscript bodies are not HTML-escaped (would break the content), but `</script>` / `</style>` / `</noscript>` and `<!--` are rewritten (`<\/script>` etc.) so user content cannot break out of the wrapping tag.
157
+
158
+ ## Subpath exports
159
+
160
+ ```ts
161
+ import { useHead } from '@pyreon/head/use-head' // tree-shake fine-grained
162
+ import { HeadProvider } from '@pyreon/head/provider' // tree-shake fine-grained
163
+ import { renderWithHead } from '@pyreon/head/ssr' // SSR-only
164
+ import { HeadContext, createHeadContext } from '@pyreon/head/context' // sub-bundle-stable
165
+ ```
49
166
 
50
- ### Components
167
+ The main entry re-exports everything from `/use-head` + `/provider` for ergonomics. The `/ssr` entry is intentionally separate so client bundles don't pull in `renderToString` from `@pyreon/runtime-server`.
51
168
 
52
- - `HeadProvider` -- context provider that collects head entries from the tree
169
+ **`@pyreon/head/context` is the canonical address for `HeadContext`** across every sub-bundle. The build pipeline runs rolldown once per sub-entry (no cross-entry shared chunks), so without externalizing `HeadContext` each sub-bundle minted its own `Symbol` ID and `useContext` lookups silently missed `provide` calls from sibling bundles. Externalizing `/context` gives `HeadContext` a stable runtime address every sub-bundle resolves to. Consumers should rarely need to import directly — but if you wire a custom SSR pipeline that crosses sub-bundles (rare), use `@pyreon/head/context` for the symbol.
53
170
 
54
- ### Hooks
171
+ ## Caveats
55
172
 
56
- - `useHead(input: UseHeadInput)` -- declare head tags from any component
173
+ - `useHead()` called outside any `HeadProvider` / `renderWithHead` boundary is a **silent no-op** (does not throw).
174
+ - `useHead()` inside a `<Suspense>` child does NOT reach the document `<head>` during SSR — the head is flushed in the shell before any boundary resolves. Hoist the call into the layout / shell-level component.
175
+ - Speculation Rules are a declarative HINT. Supported browsers prefetch/prerender at their discretion; non-supporting browsers ignore the tag. It is NOT a replacement for `RouterLink prefetch`, which warms loader data.
57
176
 
58
- ### SSR
177
+ ## Documentation
59
178
 
60
- - `renderWithHead(vnode)` -- render to string and extract head tags as `RenderWithHeadResult`
61
- - `createHeadContext()` -- create a standalone head context for manual integration
179
+ Full docs: [docs.pyreon.dev/docs/head](https://docs.pyreon.dev/docs/head) (or `docs/docs/head.md` in this repo).
62
180
 
63
- ### Types
181
+ ## License
64
182
 
65
- `HeadTag`, `HeadEntry`, `UseHeadInput`, `HeadContextValue`, `HeadProviderProps`, `RenderWithHeadResult`
183
+ MIT
@@ -0,0 +1,214 @@
1
+ import { HeadContext } from "../context.js";
2
+ import { onMount, onUnmount, useContext } from "@pyreon/core";
3
+ import { effect } from "@pyreon/reactivity";
4
+
5
+ //#region src/dom.ts
6
+ const ATTR = "data-pyreon-head";
7
+ /** Tracks managed elements by key — avoids querySelectorAll on every sync */
8
+ const managedElements = /* @__PURE__ */ new Map();
9
+ /**
10
+ * Sync the resolved head tags to the real DOM <head>.
11
+ * Uses incremental diffing: matches existing elements by key, patches attributes
12
+ * in-place, adds new elements, and removes stale ones.
13
+ * Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.
14
+ * No-op on the server (typeof document === "undefined").
15
+ */
16
+ function patchExistingTag(found, tag, kept) {
17
+ kept.add(found.getAttribute(ATTR));
18
+ patchAttrs(found, tag.props);
19
+ const content = String(tag.children);
20
+ if (found.textContent !== content) found.textContent = content;
21
+ }
22
+ function createNewTag(tag) {
23
+ if (typeof document === "undefined") return;
24
+ const el = document.createElement(tag.tag);
25
+ const key = tag.key;
26
+ el.setAttribute(ATTR, key);
27
+ for (const [k, v] of Object.entries(tag.props)) el.setAttribute(k, v);
28
+ if (tag.children) el.textContent = tag.children;
29
+ document.head.appendChild(el);
30
+ managedElements.set(key, el);
31
+ }
32
+ function syncDom(ctx) {
33
+ if (typeof document === "undefined") return;
34
+ const tags = ctx.resolve();
35
+ const titleTemplate = ctx.resolveTitleTemplate();
36
+ let needsSeed = managedElements.size === 0;
37
+ if (!needsSeed) {
38
+ const sample = managedElements.values().next().value;
39
+ if (sample && !sample.isConnected) {
40
+ managedElements.clear();
41
+ needsSeed = true;
42
+ }
43
+ }
44
+ if (needsSeed) {
45
+ const existing = document.head.querySelectorAll(`[${ATTR}]`);
46
+ for (const el of existing) managedElements.set(el.getAttribute(ATTR), el);
47
+ }
48
+ const kept = /* @__PURE__ */ new Set();
49
+ for (const tag of tags) {
50
+ if (tag.tag === "title") {
51
+ document.title = applyTitleTemplate(String(tag.children), titleTemplate);
52
+ continue;
53
+ }
54
+ const key = tag.key;
55
+ const found = managedElements.get(key);
56
+ if (found && found.tagName.toLowerCase() === tag.tag) patchExistingTag(found, tag, kept);
57
+ else {
58
+ if (found) {
59
+ found.remove();
60
+ managedElements.delete(key);
61
+ }
62
+ createNewTag(tag);
63
+ kept.add(key);
64
+ }
65
+ }
66
+ for (const [key, el] of managedElements) if (!kept.has(key)) {
67
+ el.remove();
68
+ managedElements.delete(key);
69
+ }
70
+ syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs());
71
+ syncElementAttrs(document.body, ctx.resolveBodyAttrs());
72
+ }
73
+ /** Patch an element's attributes to match the desired props. */
74
+ function patchAttrs(el, props) {
75
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
76
+ const attr = el.attributes[i];
77
+ if (!attr || attr.name === ATTR) continue;
78
+ if (!(attr.name in props)) el.removeAttribute(attr.name);
79
+ }
80
+ for (const [k, v] of Object.entries(props)) if (el.getAttribute(k) !== v) el.setAttribute(k, v);
81
+ }
82
+ function applyTitleTemplate(title, template) {
83
+ if (!template) return title;
84
+ if (typeof template === "function") return template(title);
85
+ return template.replace(/%s/g, title);
86
+ }
87
+ /** Sync pyreon-managed attributes on <html> or <body>. */
88
+ function syncElementAttrs(el, attrs) {
89
+ const managed = el.getAttribute(`${ATTR}-attrs`);
90
+ if (managed) {
91
+ for (const name of managed.split(",")) if (name && !(name in attrs)) el.removeAttribute(name);
92
+ }
93
+ const keys = [];
94
+ for (const [k, v] of Object.entries(attrs)) {
95
+ keys.push(k);
96
+ if (el.getAttribute(k) !== v) el.setAttribute(k, v);
97
+ }
98
+ if (keys.length > 0) el.setAttribute(`${ATTR}-attrs`, keys.join(","));
99
+ else if (managed) el.removeAttribute(`${ATTR}-attrs`);
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/use-head.ts
104
+ /** Cast a strict tag interface to the internal props format, stripping undefined values */
105
+ function toProps(obj) {
106
+ const result = {};
107
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) result[k] = v;
108
+ return result;
109
+ }
110
+ function buildEntry(o) {
111
+ const tags = [];
112
+ if (o.title != null) tags.push({
113
+ tag: "title",
114
+ key: "title",
115
+ children: o.title
116
+ });
117
+ o.meta?.forEach((m, i) => {
118
+ tags.push({
119
+ tag: "meta",
120
+ key: m.name ?? m.property ?? `meta-${i}`,
121
+ props: toProps(m)
122
+ });
123
+ });
124
+ o.link?.forEach((l, i) => {
125
+ tags.push({
126
+ tag: "link",
127
+ key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
128
+ props: toProps(l)
129
+ });
130
+ });
131
+ o.script?.forEach((s, i) => {
132
+ const { children, ...rest } = s;
133
+ tags.push({
134
+ tag: "script",
135
+ key: s.src ?? `script-${i}`,
136
+ props: toProps(rest),
137
+ ...children != null ? { children } : {}
138
+ });
139
+ });
140
+ o.style?.forEach((s, i) => {
141
+ const { children, ...rest } = s;
142
+ tags.push({
143
+ tag: "style",
144
+ key: `style-${i}`,
145
+ props: toProps(rest),
146
+ children
147
+ });
148
+ });
149
+ o.noscript?.forEach((ns, i) => {
150
+ tags.push({
151
+ tag: "noscript",
152
+ key: `noscript-${i}`,
153
+ children: ns.children
154
+ });
155
+ });
156
+ if (o.jsonLd) tags.push({
157
+ tag: "script",
158
+ key: "jsonld",
159
+ props: { type: "application/ld+json" },
160
+ children: JSON.stringify(o.jsonLd)
161
+ });
162
+ if (o.speculationRules) tags.push({
163
+ tag: "script",
164
+ key: "speculationrules",
165
+ props: { type: "speculationrules" },
166
+ children: JSON.stringify(o.speculationRules)
167
+ });
168
+ if (o.base) tags.push({
169
+ tag: "base",
170
+ key: "base",
171
+ props: toProps(o.base)
172
+ });
173
+ return {
174
+ tags,
175
+ titleTemplate: o.titleTemplate,
176
+ htmlAttrs: o.htmlAttrs,
177
+ bodyAttrs: o.bodyAttrs
178
+ };
179
+ }
180
+ /**
181
+ * Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
182
+ * for the current component.
183
+ *
184
+ * Accepts a static object or a reactive getter:
185
+ * useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
186
+ * useHead(() => ({ title: `${count()} items` })) // updates when signal changes
187
+ *
188
+ * Tags are deduplicated by key — innermost component wins.
189
+ * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
190
+ */
191
+ function useHead(input) {
192
+ const ctx = useContext(HeadContext);
193
+ if (!ctx) return;
194
+ const id = Symbol();
195
+ if (typeof input === "function") if (typeof document !== "undefined") effect(() => {
196
+ ctx.add(id, buildEntry(input()));
197
+ syncDom(ctx);
198
+ });
199
+ else ctx.add(id, buildEntry(input()));
200
+ else {
201
+ ctx.add(id, buildEntry(input));
202
+ onMount(() => {
203
+ syncDom(ctx);
204
+ });
205
+ }
206
+ onUnmount(() => {
207
+ ctx.remove(id);
208
+ syncDom(ctx);
209
+ });
210
+ }
211
+
212
+ //#endregion
213
+ export { useHead as t };
214
+ //# sourceMappingURL=use-head-B8n30QMl.js.map
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"4825d939-1","name":"context.ts"},{"uid":"4825d939-3","name":"provider.ts"},{"uid":"4825d939-5","name":"dom.ts"},{"uid":"4825d939-7","name":"use-head.ts"},{"uid":"4825d939-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"4825d939-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"4825d939-0"},"4825d939-3":{"renderedLength":2121,"gzipLength":1074,"brotliLength":0,"metaUid":"4825d939-2"},"4825d939-5":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"4825d939-4"},"4825d939-7":{"renderedLength":2634,"gzipLength":1093,"brotliLength":0,"metaUid":"4825d939-6"},"4825d939-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"4825d939-8"}},"nodeMetas":{"4825d939-0":{"id":"/src/context.ts","moduleParts":{"index.js":"4825d939-1"},"imported":[{"uid":"4825d939-10"}],"importedBy":[{"uid":"4825d939-8"},{"uid":"4825d939-2"},{"uid":"4825d939-6"}]},"4825d939-2":{"id":"/src/provider.ts","moduleParts":{"index.js":"4825d939-3"},"imported":[{"uid":"4825d939-10"},{"uid":"4825d939-0"}],"importedBy":[{"uid":"4825d939-8"}]},"4825d939-4":{"id":"/src/dom.ts","moduleParts":{"index.js":"4825d939-5"},"imported":[],"importedBy":[{"uid":"4825d939-6"}]},"4825d939-6":{"id":"/src/use-head.ts","moduleParts":{"index.js":"4825d939-7"},"imported":[{"uid":"4825d939-10"},{"uid":"4825d939-11"},{"uid":"4825d939-0"},{"uid":"4825d939-4"}],"importedBy":[{"uid":"4825d939-8"}]},"4825d939-8":{"id":"/src/index.ts","moduleParts":{"index.js":"4825d939-9"},"imported":[{"uid":"4825d939-0"},{"uid":"4825d939-2"},{"uid":"4825d939-6"}],"importedBy":[],"isEntry":true},"4825d939-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"4825d939-0"},{"uid":"4825d939-2"},{"uid":"4825d939-6"}]},"4825d939-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"4825d939-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"context.js","children":[{"name":"src/context.ts","uid":"5cba3d96-1"}]},{"name":"index.js","children":[{"name":"src/index.ts","uid":"5cba3d96-3"}]},{"name":"provider.js","children":[{"name":"src/provider.ts","uid":"5cba3d96-5"}]},{"name":"ssr.js","children":[{"name":"src/ssr.ts","uid":"5cba3d96-7"}]},{"name":"use-head.js","uid":"5cba3d96-9"},{"name":"_chunks/use-head-B8n30QMl.js","children":[{"name":"src","children":[{"uid":"5cba3d96-11","name":"dom.ts"},{"uid":"5cba3d96-12","name":"use-head.ts"}]}]}],"isRoot":true},"nodeParts":{"5cba3d96-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"5cba3d96-0"},"5cba3d96-3":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"5cba3d96-2"},"5cba3d96-5":{"renderedLength":2121,"gzipLength":1074,"brotliLength":0,"metaUid":"5cba3d96-4"},"5cba3d96-7":{"renderedLength":1370,"gzipLength":701,"brotliLength":0,"metaUid":"5cba3d96-6"},"5cba3d96-9":{"id":"use-head.js","gzipLength":104,"brotliLength":0,"renderedLength":106,"metaUid":"5cba3d96-8"},"5cba3d96-11":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"5cba3d96-10"},"5cba3d96-12":{"renderedLength":2634,"gzipLength":1093,"brotliLength":0,"metaUid":"5cba3d96-8"}},"nodeMetas":{"5cba3d96-0":{"id":"/src/context.ts","moduleParts":{"context.js":"5cba3d96-1"},"imported":[{"uid":"5cba3d96-13"}],"importedBy":[{"uid":"5cba3d96-2"},{"uid":"5cba3d96-4"},{"uid":"5cba3d96-8"},{"uid":"5cba3d96-6"}],"isEntry":true},"5cba3d96-2":{"id":"/src/index.ts","moduleParts":{"index.js":"5cba3d96-3"},"imported":[{"uid":"5cba3d96-0"},{"uid":"5cba3d96-4"},{"uid":"5cba3d96-8"}],"importedBy":[],"isEntry":true},"5cba3d96-4":{"id":"/src/provider.ts","moduleParts":{"provider.js":"5cba3d96-5"},"imported":[{"uid":"5cba3d96-13"},{"uid":"5cba3d96-0"}],"importedBy":[{"uid":"5cba3d96-2"}],"isEntry":true},"5cba3d96-6":{"id":"/src/ssr.ts","moduleParts":{"ssr.js":"5cba3d96-7"},"imported":[{"uid":"5cba3d96-13"},{"uid":"5cba3d96-15"},{"uid":"5cba3d96-0"}],"importedBy":[],"isEntry":true},"5cba3d96-8":{"id":"/src/use-head.ts","moduleParts":{"use-head.js":"5cba3d96-9","_chunks/use-head-B8n30QMl.js":"5cba3d96-12"},"imported":[{"uid":"5cba3d96-13"},{"uid":"5cba3d96-14"},{"uid":"5cba3d96-0"},{"uid":"5cba3d96-10"}],"importedBy":[{"uid":"5cba3d96-2"}],"isEntry":true},"5cba3d96-10":{"id":"/src/dom.ts","moduleParts":{"_chunks/use-head-B8n30QMl.js":"5cba3d96-11"},"imported":[],"importedBy":[{"uid":"5cba3d96-8"}]},"5cba3d96-13":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"5cba3d96-0"},{"uid":"5cba3d96-4"},{"uid":"5cba3d96-8"},{"uid":"5cba3d96-6"}]},"5cba3d96-14":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"5cba3d96-8"}]},"5cba3d96-15":{"id":"@pyreon/runtime-server","moduleParts":{},"imported":[],"importedBy":[{"uid":"5cba3d96-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/context.js ADDED
@@ -0,0 +1,62 @@
1
+ import { createContext } from "@pyreon/core";
2
+
3
+ //#region src/context.ts
4
+ function createHeadContext() {
5
+ const map = /* @__PURE__ */ new Map();
6
+ let dirty = true;
7
+ let cachedTags = [];
8
+ let cachedTitleTemplate;
9
+ let cachedHtmlAttrs = {};
10
+ let cachedBodyAttrs = {};
11
+ function rebuild() {
12
+ if (!dirty) return;
13
+ dirty = false;
14
+ const keyed = /* @__PURE__ */ new Map();
15
+ const unkeyed = [];
16
+ let titleTemplate;
17
+ const htmlAttrs = {};
18
+ const bodyAttrs = {};
19
+ for (const entry of map.values()) {
20
+ for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
21
+ else unkeyed.push(tag);
22
+ if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
23
+ if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
24
+ if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
25
+ }
26
+ cachedTags = [...keyed.values(), ...unkeyed];
27
+ cachedTitleTemplate = titleTemplate;
28
+ cachedHtmlAttrs = htmlAttrs;
29
+ cachedBodyAttrs = bodyAttrs;
30
+ }
31
+ return {
32
+ add(id, entry) {
33
+ map.set(id, entry);
34
+ dirty = true;
35
+ },
36
+ remove(id) {
37
+ map.delete(id);
38
+ dirty = true;
39
+ },
40
+ resolve() {
41
+ rebuild();
42
+ return cachedTags;
43
+ },
44
+ resolveTitleTemplate() {
45
+ rebuild();
46
+ return cachedTitleTemplate;
47
+ },
48
+ resolveHtmlAttrs() {
49
+ rebuild();
50
+ return cachedHtmlAttrs;
51
+ },
52
+ resolveBodyAttrs() {
53
+ rebuild();
54
+ return cachedBodyAttrs;
55
+ }
56
+ };
57
+ }
58
+ const HeadContext = createContext(null);
59
+
60
+ //#endregion
61
+ export { HeadContext, createHeadContext };
62
+ //# sourceMappingURL=context.js.map