@pyreon/head 0.22.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/index.js +4 -260
- package/lib/provider.js +1 -1
- package/lib/ssr.js +1 -1
- package/lib/types/index.d.ts +2 -1
- package/lib/use-head.js +3 -213
- package/package.json +7 -7
- package/src/index.ts +1 -6
- package/src/provider.ts +1 -4
- package/src/ssr.ts +1 -10
- package/src/tests/context-identity.test.ts +67 -91
- package/src/use-head.ts +1 -4
- package/lib/analysis/context.js.html +0 -5406
- 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/lib/index.js
CHANGED
|
@@ -1,261 +1,5 @@
|
|
|
1
|
-
import { HeadContext,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { HeadContext, createHeadContext } from "./context.js";
|
|
2
|
+
import { HeadProvider } from "./provider.js";
|
|
3
|
+
import { t as useHead } from "./_chunks/use-head-B8n30QMl.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Provides a HeadContextValue to all descendant components.
|
|
8
|
-
* Wrap your app root with this to enable useHead() throughout the tree.
|
|
9
|
-
*
|
|
10
|
-
* Resolution order (first non-null wins):
|
|
11
|
-
* 1. `props.context` — explicit context (documented SSR pattern).
|
|
12
|
-
* 2. An outer `HeadContext` already in scope — inherited transparently.
|
|
13
|
-
* This is what makes `renderWithHead(h(HeadProvider, null, h(App)))`
|
|
14
|
-
* work without manual context plumbing: `renderWithHead` pushes its
|
|
15
|
-
* own `HeadContext` onto the per-request stack, and a nested
|
|
16
|
-
* `HeadProvider` (e.g. one zero's `App` renders unconditionally)
|
|
17
|
-
* inherits it instead of silently shadowing it with a fresh,
|
|
18
|
-
* write-only registry.
|
|
19
|
-
* 3. A freshly-created `HeadContext` — root-level fallback (pure CSR).
|
|
20
|
-
*
|
|
21
|
-
* The inheritance step is load-bearing for any consumer wrapping
|
|
22
|
-
* `<HeadProvider>` inside `renderWithHead()` (the documented JSDoc
|
|
23
|
-
* pattern below) AND for the SSG / runtime-SSR pipeline in `@pyreon/zero`,
|
|
24
|
-
* whose `createApp` always mounts `h(HeadProvider, null, …)` with no
|
|
25
|
-
* `context` prop. Without inheritance, all `useHead()` calls in the
|
|
26
|
-
* subtree wrote tags into the inner ctx while `renderWithHead` resolved
|
|
27
|
-
* the outer ctx — producing an empty `<head>` for the whole app.
|
|
28
|
-
*
|
|
29
|
-
* Apps that genuinely need an isolated registry (e.g. iframe / micro-
|
|
30
|
-
* frontend boundaries) can still opt out by passing
|
|
31
|
-
* `context={createHeadContext()}` explicitly — `props.context` always wins.
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* // Auto-create context (root of a CSR app):
|
|
35
|
-
* <HeadProvider><App /></HeadProvider>
|
|
36
|
-
*
|
|
37
|
-
* // Explicit context (e.g. for SSR):
|
|
38
|
-
* const headCtx = createHeadContext()
|
|
39
|
-
* mount(h(HeadProvider, { context: headCtx }, h(App, null)), root)
|
|
40
|
-
*
|
|
41
|
-
* // Composes with `renderWithHead` out of the box — no plumbing needed:
|
|
42
|
-
* const { html, head } = await renderWithHead(h(HeadProvider, null, h(App, null)))
|
|
43
|
-
*/
|
|
44
|
-
const HeadProvider = (props) => {
|
|
45
|
-
provide(HeadContext$1, props.context ?? useContext(HeadContext$1) ?? createHeadContext$1());
|
|
46
|
-
const ch = props.children;
|
|
47
|
-
return typeof ch === "function" ? ch() : ch;
|
|
48
|
-
};
|
|
49
|
-
nativeCompat(HeadProvider);
|
|
50
|
-
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/dom.ts
|
|
53
|
-
const ATTR = "data-pyreon-head";
|
|
54
|
-
/** Tracks managed elements by key — avoids querySelectorAll on every sync */
|
|
55
|
-
const managedElements = /* @__PURE__ */ new Map();
|
|
56
|
-
/**
|
|
57
|
-
* Sync the resolved head tags to the real DOM <head>.
|
|
58
|
-
* Uses incremental diffing: matches existing elements by key, patches attributes
|
|
59
|
-
* in-place, adds new elements, and removes stale ones.
|
|
60
|
-
* Also syncs htmlAttrs, bodyAttrs, and applies titleTemplate.
|
|
61
|
-
* No-op on the server (typeof document === "undefined").
|
|
62
|
-
*/
|
|
63
|
-
function patchExistingTag(found, tag, kept) {
|
|
64
|
-
kept.add(found.getAttribute(ATTR));
|
|
65
|
-
patchAttrs(found, tag.props);
|
|
66
|
-
const content = String(tag.children);
|
|
67
|
-
if (found.textContent !== content) found.textContent = content;
|
|
68
|
-
}
|
|
69
|
-
function createNewTag(tag) {
|
|
70
|
-
if (typeof document === "undefined") return;
|
|
71
|
-
const el = document.createElement(tag.tag);
|
|
72
|
-
const key = tag.key;
|
|
73
|
-
el.setAttribute(ATTR, key);
|
|
74
|
-
for (const [k, v] of Object.entries(tag.props)) el.setAttribute(k, v);
|
|
75
|
-
if (tag.children) el.textContent = tag.children;
|
|
76
|
-
document.head.appendChild(el);
|
|
77
|
-
managedElements.set(key, el);
|
|
78
|
-
}
|
|
79
|
-
function syncDom(ctx) {
|
|
80
|
-
if (typeof document === "undefined") return;
|
|
81
|
-
const tags = ctx.resolve();
|
|
82
|
-
const titleTemplate = ctx.resolveTitleTemplate();
|
|
83
|
-
let needsSeed = managedElements.size === 0;
|
|
84
|
-
if (!needsSeed) {
|
|
85
|
-
const sample = managedElements.values().next().value;
|
|
86
|
-
if (sample && !sample.isConnected) {
|
|
87
|
-
managedElements.clear();
|
|
88
|
-
needsSeed = true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (needsSeed) {
|
|
92
|
-
const existing = document.head.querySelectorAll(`[${ATTR}]`);
|
|
93
|
-
for (const el of existing) managedElements.set(el.getAttribute(ATTR), el);
|
|
94
|
-
}
|
|
95
|
-
const kept = /* @__PURE__ */ new Set();
|
|
96
|
-
for (const tag of tags) {
|
|
97
|
-
if (tag.tag === "title") {
|
|
98
|
-
document.title = applyTitleTemplate(String(tag.children), titleTemplate);
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const key = tag.key;
|
|
102
|
-
const found = managedElements.get(key);
|
|
103
|
-
if (found && found.tagName.toLowerCase() === tag.tag) patchExistingTag(found, tag, kept);
|
|
104
|
-
else {
|
|
105
|
-
if (found) {
|
|
106
|
-
found.remove();
|
|
107
|
-
managedElements.delete(key);
|
|
108
|
-
}
|
|
109
|
-
createNewTag(tag);
|
|
110
|
-
kept.add(key);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
for (const [key, el] of managedElements) if (!kept.has(key)) {
|
|
114
|
-
el.remove();
|
|
115
|
-
managedElements.delete(key);
|
|
116
|
-
}
|
|
117
|
-
syncElementAttrs(document.documentElement, ctx.resolveHtmlAttrs());
|
|
118
|
-
syncElementAttrs(document.body, ctx.resolveBodyAttrs());
|
|
119
|
-
}
|
|
120
|
-
/** Patch an element's attributes to match the desired props. */
|
|
121
|
-
function patchAttrs(el, props) {
|
|
122
|
-
for (let i = el.attributes.length - 1; i >= 0; i--) {
|
|
123
|
-
const attr = el.attributes[i];
|
|
124
|
-
if (!attr || attr.name === ATTR) continue;
|
|
125
|
-
if (!(attr.name in props)) el.removeAttribute(attr.name);
|
|
126
|
-
}
|
|
127
|
-
for (const [k, v] of Object.entries(props)) if (el.getAttribute(k) !== v) el.setAttribute(k, v);
|
|
128
|
-
}
|
|
129
|
-
function applyTitleTemplate(title, template) {
|
|
130
|
-
if (!template) return title;
|
|
131
|
-
if (typeof template === "function") return template(title);
|
|
132
|
-
return template.replace(/%s/g, title);
|
|
133
|
-
}
|
|
134
|
-
/** Sync pyreon-managed attributes on <html> or <body>. */
|
|
135
|
-
function syncElementAttrs(el, attrs) {
|
|
136
|
-
const managed = el.getAttribute(`${ATTR}-attrs`);
|
|
137
|
-
if (managed) {
|
|
138
|
-
for (const name of managed.split(",")) if (name && !(name in attrs)) el.removeAttribute(name);
|
|
139
|
-
}
|
|
140
|
-
const keys = [];
|
|
141
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
142
|
-
keys.push(k);
|
|
143
|
-
if (el.getAttribute(k) !== v) el.setAttribute(k, v);
|
|
144
|
-
}
|
|
145
|
-
if (keys.length > 0) el.setAttribute(`${ATTR}-attrs`, keys.join(","));
|
|
146
|
-
else if (managed) el.removeAttribute(`${ATTR}-attrs`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region src/use-head.ts
|
|
151
|
-
/** Cast a strict tag interface to the internal props format, stripping undefined values */
|
|
152
|
-
function toProps(obj) {
|
|
153
|
-
const result = {};
|
|
154
|
-
for (const [k, v] of Object.entries(obj)) if (v !== void 0) result[k] = v;
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
157
|
-
function buildEntry(o) {
|
|
158
|
-
const tags = [];
|
|
159
|
-
if (o.title != null) tags.push({
|
|
160
|
-
tag: "title",
|
|
161
|
-
key: "title",
|
|
162
|
-
children: o.title
|
|
163
|
-
});
|
|
164
|
-
o.meta?.forEach((m, i) => {
|
|
165
|
-
tags.push({
|
|
166
|
-
tag: "meta",
|
|
167
|
-
key: m.name ?? m.property ?? `meta-${i}`,
|
|
168
|
-
props: toProps(m)
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
o.link?.forEach((l, i) => {
|
|
172
|
-
tags.push({
|
|
173
|
-
tag: "link",
|
|
174
|
-
key: l.href ? `link-${l.rel || ""}-${l.href}` : l.rel ? `link-${l.rel}` : `link-${i}`,
|
|
175
|
-
props: toProps(l)
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
o.script?.forEach((s, i) => {
|
|
179
|
-
const { children, ...rest } = s;
|
|
180
|
-
tags.push({
|
|
181
|
-
tag: "script",
|
|
182
|
-
key: s.src ?? `script-${i}`,
|
|
183
|
-
props: toProps(rest),
|
|
184
|
-
...children != null ? { children } : {}
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
o.style?.forEach((s, i) => {
|
|
188
|
-
const { children, ...rest } = s;
|
|
189
|
-
tags.push({
|
|
190
|
-
tag: "style",
|
|
191
|
-
key: `style-${i}`,
|
|
192
|
-
props: toProps(rest),
|
|
193
|
-
children
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
o.noscript?.forEach((ns, i) => {
|
|
197
|
-
tags.push({
|
|
198
|
-
tag: "noscript",
|
|
199
|
-
key: `noscript-${i}`,
|
|
200
|
-
children: ns.children
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
if (o.jsonLd) tags.push({
|
|
204
|
-
tag: "script",
|
|
205
|
-
key: "jsonld",
|
|
206
|
-
props: { type: "application/ld+json" },
|
|
207
|
-
children: JSON.stringify(o.jsonLd)
|
|
208
|
-
});
|
|
209
|
-
if (o.speculationRules) tags.push({
|
|
210
|
-
tag: "script",
|
|
211
|
-
key: "speculationrules",
|
|
212
|
-
props: { type: "speculationrules" },
|
|
213
|
-
children: JSON.stringify(o.speculationRules)
|
|
214
|
-
});
|
|
215
|
-
if (o.base) tags.push({
|
|
216
|
-
tag: "base",
|
|
217
|
-
key: "base",
|
|
218
|
-
props: toProps(o.base)
|
|
219
|
-
});
|
|
220
|
-
return {
|
|
221
|
-
tags,
|
|
222
|
-
titleTemplate: o.titleTemplate,
|
|
223
|
-
htmlAttrs: o.htmlAttrs,
|
|
224
|
-
bodyAttrs: o.bodyAttrs
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Register head tags (title, meta, link, script, style, noscript, base, jsonLd)
|
|
229
|
-
* for the current component.
|
|
230
|
-
*
|
|
231
|
-
* Accepts a static object or a reactive getter:
|
|
232
|
-
* useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
|
|
233
|
-
* useHead(() => ({ title: `${count()} items` })) // updates when signal changes
|
|
234
|
-
*
|
|
235
|
-
* Tags are deduplicated by key — innermost component wins.
|
|
236
|
-
* Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
|
|
237
|
-
*/
|
|
238
|
-
function useHead(input) {
|
|
239
|
-
const ctx = useContext(HeadContext$1);
|
|
240
|
-
if (!ctx) return;
|
|
241
|
-
const id = Symbol();
|
|
242
|
-
if (typeof input === "function") if (typeof document !== "undefined") effect(() => {
|
|
243
|
-
ctx.add(id, buildEntry(input()));
|
|
244
|
-
syncDom(ctx);
|
|
245
|
-
});
|
|
246
|
-
else ctx.add(id, buildEntry(input()));
|
|
247
|
-
else {
|
|
248
|
-
ctx.add(id, buildEntry(input));
|
|
249
|
-
onMount(() => {
|
|
250
|
-
syncDom(ctx);
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
onUnmount(() => {
|
|
254
|
-
ctx.remove(id);
|
|
255
|
-
syncDom(ctx);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
//#endregion
|
|
260
|
-
export { HeadContext, HeadProvider, createHeadContext, useHead };
|
|
261
|
-
//# sourceMappingURL=index.js.map
|
|
5
|
+
export { HeadContext, HeadProvider, createHeadContext, useHead };
|
package/lib/provider.js
CHANGED
package/lib/ssr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { HeadContext, createHeadContext } from "./context.js";
|
|
1
2
|
import { h, pushContext } from "@pyreon/core";
|
|
2
3
|
import { renderToString } from "@pyreon/runtime-server";
|
|
3
|
-
import { HeadContext, createHeadContext } from "@pyreon/head/context";
|
|
4
4
|
|
|
5
5
|
//#region src/ssr.ts
|
|
6
6
|
const VOID_TAGS = new Set([
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { HeadContext, createHeadContext } from "@pyreon/head/context";
|
|
2
1
|
import { ComponentFn, Props, VNodeChild } from "@pyreon/core";
|
|
3
2
|
|
|
4
3
|
//#region src/context.d.ts
|
|
@@ -201,6 +200,8 @@ interface HeadContextValue {
|
|
|
201
200
|
/** Returns merged bodyAttrs (later entries override earlier) */
|
|
202
201
|
resolveBodyAttrs(): Record<string, string>;
|
|
203
202
|
}
|
|
203
|
+
declare function createHeadContext(): HeadContextValue;
|
|
204
|
+
declare const HeadContext: import("@pyreon/core").Context<HeadContextValue | null>;
|
|
204
205
|
//#endregion
|
|
205
206
|
//#region src/provider.d.ts
|
|
206
207
|
interface HeadProviderProps extends Props {
|
package/lib/use-head.js
CHANGED
|
@@ -1,214 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { HeadContext } from "@pyreon/head/context";
|
|
1
|
+
import "./context.js";
|
|
2
|
+
import { t as useHead } from "./_chunks/use-head-B8n30QMl.js";
|
|
4
3
|
|
|
5
|
-
|
|
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 };
|
|
214
|
-
//# sourceMappingURL=use-head.js.map
|
|
4
|
+
export { useHead };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/head",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Head tag management for Pyreon — works in SSR and CSR",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/head#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
"prepublishOnly": "bun run build"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@pyreon/core": "^0.
|
|
68
|
-
"@pyreon/reactivity": "^0.
|
|
69
|
-
"@pyreon/runtime-server": "^0.
|
|
67
|
+
"@pyreon/core": "^0.23.0",
|
|
68
|
+
"@pyreon/reactivity": "^0.23.0",
|
|
69
|
+
"@pyreon/runtime-server": "^0.23.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@happy-dom/global-registrator": "^20.8.9",
|
|
73
73
|
"@pyreon/manifest": "0.13.1",
|
|
74
|
-
"@pyreon/runtime-dom": "^0.
|
|
75
|
-
"@pyreon/runtime-server": "^0.
|
|
76
|
-
"@pyreon/test-utils": "^0.13.
|
|
74
|
+
"@pyreon/runtime-dom": "^0.23.0",
|
|
75
|
+
"@pyreon/runtime-server": "^0.23.0",
|
|
76
|
+
"@pyreon/test-utils": "^0.13.10",
|
|
77
77
|
"@vitest/browser-playwright": "^4.1.4"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
package/src/index.ts
CHANGED
|
@@ -12,12 +12,7 @@ export type {
|
|
|
12
12
|
StyleTag,
|
|
13
13
|
UseHeadInput,
|
|
14
14
|
} from './context'
|
|
15
|
-
|
|
16
|
-
// the symbol — main entry + every sub-entry resolves to the same
|
|
17
|
-
// `lib/context.js` at runtime. The type re-exports above stay as `./context`
|
|
18
|
-
// (types erase, externalization doesn't apply). See `ssr.ts` for the full
|
|
19
|
-
// rationale + `tests/context-identity.test.ts` for the post-build contract.
|
|
20
|
-
export { createHeadContext, HeadContext } from '@pyreon/head/context'
|
|
15
|
+
export { createHeadContext, HeadContext } from './context'
|
|
21
16
|
export type { HeadProviderProps } from './provider'
|
|
22
17
|
export { HeadProvider } from './provider'
|
|
23
18
|
export { useHead } from './use-head'
|
package/src/provider.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { ComponentFn, Props, VNodeChild } from '@pyreon/core'
|
|
2
2
|
import { nativeCompat, provide, useContext } from '@pyreon/core'
|
|
3
3
|
import type { HeadContextValue } from './context'
|
|
4
|
-
|
|
5
|
-
// the symbol — every sub-entry resolves to the same `lib/context.js` at
|
|
6
|
-
// runtime. See `ssr.ts` for the full rationale + `tests/context-identity.test.ts`.
|
|
7
|
-
import { createHeadContext, HeadContext } from '@pyreon/head/context'
|
|
4
|
+
import { createHeadContext, HeadContext } from './context'
|
|
8
5
|
|
|
9
6
|
export interface HeadProviderProps extends Props {
|
|
10
7
|
context?: HeadContextValue | undefined
|
package/src/ssr.ts
CHANGED
|
@@ -2,16 +2,7 @@ import type { ComponentFn, VNode } from '@pyreon/core'
|
|
|
2
2
|
import { h, pushContext } from '@pyreon/core'
|
|
3
3
|
import { renderToString } from '@pyreon/runtime-server'
|
|
4
4
|
import type { HeadTag } from './context'
|
|
5
|
-
|
|
6
|
-
// emits an external `import` instead of inlining `createContext(null)`
|
|
7
|
-
// into this sub-bundle. The bundler externalizes `@pyreon/head/context`
|
|
8
|
-
// per the package's `build.external` config — every sub-entry resolves to
|
|
9
|
-
// the SAME `lib/context.js` at runtime, so `HeadContext` is one Symbol
|
|
10
|
-
// across `lib/index.js` / `lib/ssr.js` / `lib/use-head.js` / `lib/provider.js`.
|
|
11
|
-
// See `tests/context-identity.test.ts` for the post-build identity contract.
|
|
12
|
-
// Type-only imports (`HeadTag` above) keep the relative path — types erase
|
|
13
|
-
// at build, no externalization needed.
|
|
14
|
-
import { createHeadContext, HeadContext } from '@pyreon/head/context'
|
|
5
|
+
import { createHeadContext, HeadContext } from './context'
|
|
15
6
|
|
|
16
7
|
const VOID_TAGS = new Set(['meta', 'link', 'base'])
|
|
17
8
|
|