@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/lib/index.js CHANGED
@@ -1,261 +1,5 @@
1
- import { HeadContext, HeadContext as HeadContext$1, createHeadContext, createHeadContext as createHeadContext$1 } from "@pyreon/head/context";
2
- import { nativeCompat, onMount, onUnmount, provide, useContext } from "@pyreon/core";
3
- import { effect } from "@pyreon/reactivity";
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
- //#region src/provider.ts
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
@@ -1,5 +1,5 @@
1
+ import { HeadContext, createHeadContext } from "./context.js";
1
2
  import { nativeCompat, provide, useContext } from "@pyreon/core";
2
- import { HeadContext, createHeadContext } from "@pyreon/head/context";
3
3
 
4
4
  //#region src/provider.ts
5
5
  /**
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([
@@ -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 { onMount, onUnmount, useContext } from "@pyreon/core";
2
- import { effect } from "@pyreon/reactivity";
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
- //#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 };
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.22.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.22.0",
68
- "@pyreon/reactivity": "^0.22.0",
69
- "@pyreon/runtime-server": "^0.22.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.22.0",
75
- "@pyreon/runtime-server": "^0.22.0",
76
- "@pyreon/test-utils": "^0.13.9",
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
- // Runtime VALUE re-export via self-package path so the build externalizes
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
- // Runtime VALUE import via self-package path so the build externalizes
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
- // Runtime VALUE imports go through the self-package path so the build
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