@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 +146 -28
- package/lib/_chunks/use-head-B8n30QMl.js +214 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/context.js +62 -0
- package/lib/index.js +4 -317
- package/lib/provider.js +2 -59
- package/lib/ssr.js +2 -59
- package/lib/types/context.d.ts +205 -0
- package/lib/use-head.js +3 -216
- package/package.json +12 -7
- package/src/tests/context-identity.test.ts +126 -0
- package/lib/analysis/provider.js.html +0 -5406
- package/lib/analysis/ssr.js.html +0 -5406
- package/lib/analysis/use-head.js.html +0 -5406
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @pyreon/head
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
function ProfilePage() {
|
|
17
23
|
useHead({
|
|
18
|
-
title: 'My
|
|
24
|
+
title: 'My Profile',
|
|
19
25
|
meta: [
|
|
20
|
-
{ name: 'description', content: '
|
|
21
|
-
{ property: 'og:title', content: 'My
|
|
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
|
-
//
|
|
30
|
-
|
|
35
|
+
// CSR
|
|
36
|
+
mount(
|
|
31
37
|
<HeadProvider>
|
|
32
|
-
<
|
|
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
|
-
##
|
|
47
|
+
## Reactive head — thunk form
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
Pass a function so signal reads re-register on change:
|
|
40
50
|
|
|
41
51
|
```tsx
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
171
|
+
## Caveats
|
|
55
172
|
|
|
56
|
-
- `useHead(
|
|
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
|
-
|
|
177
|
+
## Documentation
|
|
59
178
|
|
|
60
|
-
|
|
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
|
-
|
|
181
|
+
## License
|
|
64
182
|
|
|
65
|
-
|
|
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":"
|
|
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
|