@pyreon/head 0.20.0 → 0.22.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.
@@ -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":"c3ae39c2-1","name":"context.ts"},{"uid":"c3ae39c2-3","name":"provider.ts"},{"uid":"c3ae39c2-5","name":"dom.ts"},{"uid":"c3ae39c2-7","name":"use-head.ts"},{"uid":"c3ae39c2-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"c3ae39c2-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"c3ae39c2-0"},"c3ae39c2-3":{"renderedLength":681,"gzipLength":408,"brotliLength":0,"metaUid":"c3ae39c2-2"},"c3ae39c2-5":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"c3ae39c2-4"},"c3ae39c2-7":{"renderedLength":2634,"gzipLength":1093,"brotliLength":0,"metaUid":"c3ae39c2-6"},"c3ae39c2-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"c3ae39c2-8"}},"nodeMetas":{"c3ae39c2-0":{"id":"/src/context.ts","moduleParts":{"index.js":"c3ae39c2-1"},"imported":[{"uid":"c3ae39c2-10"}],"importedBy":[{"uid":"c3ae39c2-8"},{"uid":"c3ae39c2-2"},{"uid":"c3ae39c2-6"}]},"c3ae39c2-2":{"id":"/src/provider.ts","moduleParts":{"index.js":"c3ae39c2-3"},"imported":[{"uid":"c3ae39c2-10"},{"uid":"c3ae39c2-0"}],"importedBy":[{"uid":"c3ae39c2-8"}]},"c3ae39c2-4":{"id":"/src/dom.ts","moduleParts":{"index.js":"c3ae39c2-5"},"imported":[],"importedBy":[{"uid":"c3ae39c2-6"}]},"c3ae39c2-6":{"id":"/src/use-head.ts","moduleParts":{"index.js":"c3ae39c2-7"},"imported":[{"uid":"c3ae39c2-10"},{"uid":"c3ae39c2-11"},{"uid":"c3ae39c2-0"},{"uid":"c3ae39c2-4"}],"importedBy":[{"uid":"c3ae39c2-8"}]},"c3ae39c2-8":{"id":"/src/index.ts","moduleParts":{"index.js":"c3ae39c2-9"},"imported":[{"uid":"c3ae39c2-0"},{"uid":"c3ae39c2-2"},{"uid":"c3ae39c2-6"}],"importedBy":[],"isEntry":true},"c3ae39c2-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"c3ae39c2-0"},{"uid":"c3ae39c2-2"},{"uid":"c3ae39c2-6"}]},"c3ae39c2-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"c3ae39c2-6"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"19674450-1","name":"provider.ts"},{"uid":"19674450-3","name":"dom.ts"},{"uid":"19674450-5","name":"use-head.ts"},{"uid":"19674450-7","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"19674450-1":{"renderedLength":2127,"gzipLength":1080,"brotliLength":0,"metaUid":"19674450-0"},"19674450-3":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"19674450-2"},"19674450-5":{"renderedLength":2636,"gzipLength":1095,"brotliLength":0,"metaUid":"19674450-4"},"19674450-7":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"19674450-6"}},"nodeMetas":{"19674450-0":{"id":"/src/provider.ts","moduleParts":{"index.js":"19674450-1"},"imported":[{"uid":"19674450-9"},{"uid":"19674450-8"}],"importedBy":[{"uid":"19674450-6"}]},"19674450-2":{"id":"/src/dom.ts","moduleParts":{"index.js":"19674450-3"},"imported":[],"importedBy":[{"uid":"19674450-4"}]},"19674450-4":{"id":"/src/use-head.ts","moduleParts":{"index.js":"19674450-5"},"imported":[{"uid":"19674450-9"},{"uid":"19674450-10"},{"uid":"19674450-8"},{"uid":"19674450-2"}],"importedBy":[{"uid":"19674450-6"}]},"19674450-6":{"id":"/src/index.ts","moduleParts":{"index.js":"19674450-7"},"imported":[{"uid":"19674450-8"},{"uid":"19674450-0"},{"uid":"19674450-4"}],"importedBy":[],"isEntry":true},"19674450-8":{"id":"@pyreon/head/context","moduleParts":{},"imported":[],"importedBy":[{"uid":"19674450-6"},{"uid":"19674450-0"},{"uid":"19674450-4"}]},"19674450-9":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"19674450-0"},{"uid":"19674450-4"}]},"19674450-10":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"19674450-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -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":"provider.js","children":[{"name":"src","children":[{"uid":"65ae058d-1","name":"context.ts"},{"uid":"65ae058d-3","name":"provider.ts"}]}]}],"isRoot":true},"nodeParts":{"65ae058d-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"65ae058d-0"},"65ae058d-3":{"renderedLength":681,"gzipLength":408,"brotliLength":0,"metaUid":"65ae058d-2"}},"nodeMetas":{"65ae058d-0":{"id":"/src/context.ts","moduleParts":{"provider.js":"65ae058d-1"},"imported":[{"uid":"65ae058d-4"}],"importedBy":[{"uid":"65ae058d-2"}]},"65ae058d-2":{"id":"/src/provider.ts","moduleParts":{"provider.js":"65ae058d-3"},"imported":[{"uid":"65ae058d-4"},{"uid":"65ae058d-0"}],"importedBy":[],"isEntry":true},"65ae058d-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"65ae058d-2"},{"uid":"65ae058d-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"provider.js","children":[{"name":"src/provider.ts","uid":"5e1e2d37-1"}]}],"isRoot":true},"nodeParts":{"5e1e2d37-1":{"renderedLength":2121,"gzipLength":1074,"brotliLength":0,"metaUid":"5e1e2d37-0"}},"nodeMetas":{"5e1e2d37-0":{"id":"/src/provider.ts","moduleParts":{"provider.js":"5e1e2d37-1"},"imported":[{"uid":"5e1e2d37-2"},{"uid":"5e1e2d37-3"}],"importedBy":[],"isEntry":true},"5e1e2d37-2":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"5e1e2d37-0"}]},"5e1e2d37-3":{"id":"@pyreon/head/context","moduleParts":{},"imported":[],"importedBy":[{"uid":"5e1e2d37-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -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":"ssr.js","children":[{"name":"src","children":[{"uid":"b6fd5bb7-1","name":"context.ts"},{"uid":"b6fd5bb7-3","name":"ssr.ts"}]}]}],"isRoot":true},"nodeParts":{"b6fd5bb7-1":{"renderedLength":1373,"gzipLength":509,"brotliLength":0,"metaUid":"b6fd5bb7-0"},"b6fd5bb7-3":{"renderedLength":1370,"gzipLength":701,"brotliLength":0,"metaUid":"b6fd5bb7-2"}},"nodeMetas":{"b6fd5bb7-0":{"id":"/src/context.ts","moduleParts":{"ssr.js":"b6fd5bb7-1"},"imported":[{"uid":"b6fd5bb7-4"}],"importedBy":[{"uid":"b6fd5bb7-2"}]},"b6fd5bb7-2":{"id":"/src/ssr.ts","moduleParts":{"ssr.js":"b6fd5bb7-3"},"imported":[{"uid":"b6fd5bb7-4"},{"uid":"b6fd5bb7-5"},{"uid":"b6fd5bb7-0"}],"importedBy":[],"isEntry":true},"b6fd5bb7-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"b6fd5bb7-2"},{"uid":"b6fd5bb7-0"}]},"b6fd5bb7-5":{"id":"@pyreon/runtime-server","moduleParts":{},"imported":[],"importedBy":[{"uid":"b6fd5bb7-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"ssr.js","children":[{"name":"src/ssr.ts","uid":"6c67066c-1"}]}],"isRoot":true},"nodeParts":{"6c67066c-1":{"renderedLength":1370,"gzipLength":701,"brotliLength":0,"metaUid":"6c67066c-0"}},"nodeMetas":{"6c67066c-0":{"id":"/src/ssr.ts","moduleParts":{"ssr.js":"6c67066c-1"},"imported":[{"uid":"6c67066c-2"},{"uid":"6c67066c-3"},{"uid":"6c67066c-4"}],"importedBy":[],"isEntry":true},"6c67066c-2":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"6c67066c-0"}]},"6c67066c-3":{"id":"@pyreon/runtime-server","moduleParts":{},"imported":[],"importedBy":[{"uid":"6c67066c-0"}]},"6c67066c-4":{"id":"@pyreon/head/context","moduleParts":{},"imported":[],"importedBy":[{"uid":"6c67066c-0"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
@@ -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":"use-head.js","children":[{"name":"src","children":[{"uid":"a2d4ff75-1","name":"context.ts"},{"uid":"a2d4ff75-3","name":"dom.ts"},{"uid":"a2d4ff75-5","name":"use-head.ts"}]}]}],"isRoot":true},"nodeParts":{"a2d4ff75-1":{"renderedLength":79,"gzipLength":85,"brotliLength":0,"metaUid":"a2d4ff75-0"},"a2d4ff75-3":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"a2d4ff75-2"},"a2d4ff75-5":{"renderedLength":2634,"gzipLength":1093,"brotliLength":0,"metaUid":"a2d4ff75-4"}},"nodeMetas":{"a2d4ff75-0":{"id":"/src/context.ts","moduleParts":{"use-head.js":"a2d4ff75-1"},"imported":[{"uid":"a2d4ff75-6"}],"importedBy":[{"uid":"a2d4ff75-4"}]},"a2d4ff75-2":{"id":"/src/dom.ts","moduleParts":{"use-head.js":"a2d4ff75-3"},"imported":[],"importedBy":[{"uid":"a2d4ff75-4"}]},"a2d4ff75-4":{"id":"/src/use-head.ts","moduleParts":{"use-head.js":"a2d4ff75-5"},"imported":[{"uid":"a2d4ff75-6"},{"uid":"a2d4ff75-7"},{"uid":"a2d4ff75-0"},{"uid":"a2d4ff75-2"}],"importedBy":[],"isEntry":true},"a2d4ff75-6":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"a2d4ff75-4"},{"uid":"a2d4ff75-0"}]},"a2d4ff75-7":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"a2d4ff75-4"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"use-head.js","children":[{"name":"src","children":[{"uid":"e064605f-1","name":"dom.ts"},{"uid":"e064605f-3","name":"use-head.ts"}]}]}],"isRoot":true},"nodeParts":{"e064605f-1":{"renderedLength":3447,"gzipLength":1292,"brotliLength":0,"metaUid":"e064605f-0"},"e064605f-3":{"renderedLength":2634,"gzipLength":1093,"brotliLength":0,"metaUid":"e064605f-2"}},"nodeMetas":{"e064605f-0":{"id":"/src/dom.ts","moduleParts":{"use-head.js":"e064605f-1"},"imported":[],"importedBy":[{"uid":"e064605f-2"}]},"e064605f-2":{"id":"/src/use-head.ts","moduleParts":{"use-head.js":"e064605f-3"},"imported":[{"uid":"e064605f-4"},{"uid":"e064605f-5"},{"uid":"e064605f-6"},{"uid":"e064605f-0"}],"importedBy":[],"isEntry":true},"e064605f-4":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"e064605f-2"}]},"e064605f-5":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"e064605f-2"}]},"e064605f-6":{"id":"@pyreon/head/context","moduleParts":{},"imported":[],"importedBy":[{"uid":"e064605f-2"}]}},"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
package/lib/index.js CHANGED
@@ -1,81 +1,48 @@
1
- import { createContext, nativeCompat, onMount, onUnmount, provide, useContext } from "@pyreon/core";
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";
2
3
  import { effect } from "@pyreon/reactivity";
3
4
 
4
- //#region src/context.ts
5
- function createHeadContext() {
6
- const map = /* @__PURE__ */ new Map();
7
- let dirty = true;
8
- let cachedTags = [];
9
- let cachedTitleTemplate;
10
- let cachedHtmlAttrs = {};
11
- let cachedBodyAttrs = {};
12
- function rebuild() {
13
- if (!dirty) return;
14
- dirty = false;
15
- const keyed = /* @__PURE__ */ new Map();
16
- const unkeyed = [];
17
- let titleTemplate;
18
- const htmlAttrs = {};
19
- const bodyAttrs = {};
20
- for (const entry of map.values()) {
21
- for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
22
- else unkeyed.push(tag);
23
- if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
24
- if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
25
- if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
26
- }
27
- cachedTags = [...keyed.values(), ...unkeyed];
28
- cachedTitleTemplate = titleTemplate;
29
- cachedHtmlAttrs = htmlAttrs;
30
- cachedBodyAttrs = bodyAttrs;
31
- }
32
- return {
33
- add(id, entry) {
34
- map.set(id, entry);
35
- dirty = true;
36
- },
37
- remove(id) {
38
- map.delete(id);
39
- dirty = true;
40
- },
41
- resolve() {
42
- rebuild();
43
- return cachedTags;
44
- },
45
- resolveTitleTemplate() {
46
- rebuild();
47
- return cachedTitleTemplate;
48
- },
49
- resolveHtmlAttrs() {
50
- rebuild();
51
- return cachedHtmlAttrs;
52
- },
53
- resolveBodyAttrs() {
54
- rebuild();
55
- return cachedBodyAttrs;
56
- }
57
- };
58
- }
59
- const HeadContext = createContext(null);
60
-
61
- //#endregion
62
5
  //#region src/provider.ts
63
6
  /**
64
7
  * Provides a HeadContextValue to all descendant components.
65
8
  * Wrap your app root with this to enable useHead() throughout the tree.
66
9
  *
67
- * If no `context` prop is passed, a new HeadContext is created automatically.
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.
68
32
  *
69
33
  * @example
70
- * // Auto-create context:
34
+ * // Auto-create context (root of a CSR app):
71
35
  * <HeadProvider><App /></HeadProvider>
72
36
  *
73
37
  * // Explicit context (e.g. for SSR):
74
38
  * const headCtx = createHeadContext()
75
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)))
76
43
  */
77
44
  const HeadProvider = (props) => {
78
- provide(HeadContext, props.context ?? createHeadContext());
45
+ provide(HeadContext$1, props.context ?? useContext(HeadContext$1) ?? createHeadContext$1());
79
46
  const ch = props.children;
80
47
  return typeof ch === "function" ? ch() : ch;
81
48
  };
@@ -269,7 +236,7 @@ function buildEntry(o) {
269
236
  * Requires a <HeadProvider> (CSR) or renderWithHead() (SSR) ancestor.
270
237
  */
271
238
  function useHead(input) {
272
- const ctx = useContext(HeadContext);
239
+ const ctx = useContext(HeadContext$1);
273
240
  if (!ctx) return;
274
241
  const id = Symbol();
275
242
  if (typeof input === "function") if (typeof document !== "undefined") effect(() => {
package/lib/provider.js CHANGED
@@ -1,80 +1,47 @@
1
- import { createContext, nativeCompat, provide } from "@pyreon/core";
1
+ import { nativeCompat, provide, useContext } from "@pyreon/core";
2
+ import { HeadContext, createHeadContext } from "@pyreon/head/context";
2
3
 
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
4
  //#region src/provider.ts
62
5
  /**
63
6
  * Provides a HeadContextValue to all descendant components.
64
7
  * Wrap your app root with this to enable useHead() throughout the tree.
65
8
  *
66
- * If no `context` prop is passed, a new HeadContext is created automatically.
9
+ * Resolution order (first non-null wins):
10
+ * 1. `props.context` — explicit context (documented SSR pattern).
11
+ * 2. An outer `HeadContext` already in scope — inherited transparently.
12
+ * This is what makes `renderWithHead(h(HeadProvider, null, h(App)))`
13
+ * work without manual context plumbing: `renderWithHead` pushes its
14
+ * own `HeadContext` onto the per-request stack, and a nested
15
+ * `HeadProvider` (e.g. one zero's `App` renders unconditionally)
16
+ * inherits it instead of silently shadowing it with a fresh,
17
+ * write-only registry.
18
+ * 3. A freshly-created `HeadContext` — root-level fallback (pure CSR).
19
+ *
20
+ * The inheritance step is load-bearing for any consumer wrapping
21
+ * `<HeadProvider>` inside `renderWithHead()` (the documented JSDoc
22
+ * pattern below) AND for the SSG / runtime-SSR pipeline in `@pyreon/zero`,
23
+ * whose `createApp` always mounts `h(HeadProvider, null, …)` with no
24
+ * `context` prop. Without inheritance, all `useHead()` calls in the
25
+ * subtree wrote tags into the inner ctx while `renderWithHead` resolved
26
+ * the outer ctx — producing an empty `<head>` for the whole app.
27
+ *
28
+ * Apps that genuinely need an isolated registry (e.g. iframe / micro-
29
+ * frontend boundaries) can still opt out by passing
30
+ * `context={createHeadContext()}` explicitly — `props.context` always wins.
67
31
  *
68
32
  * @example
69
- * // Auto-create context:
33
+ * // Auto-create context (root of a CSR app):
70
34
  * <HeadProvider><App /></HeadProvider>
71
35
  *
72
36
  * // Explicit context (e.g. for SSR):
73
37
  * const headCtx = createHeadContext()
74
38
  * mount(h(HeadProvider, { context: headCtx }, h(App, null)), root)
39
+ *
40
+ * // Composes with `renderWithHead` out of the box — no plumbing needed:
41
+ * const { html, head } = await renderWithHead(h(HeadProvider, null, h(App, null)))
75
42
  */
76
43
  const HeadProvider = (props) => {
77
- provide(HeadContext, props.context ?? createHeadContext());
44
+ provide(HeadContext, props.context ?? useContext(HeadContext) ?? createHeadContext());
78
45
  const ch = props.children;
79
46
  return typeof ch === "function" ? ch() : ch;
80
47
  };
package/lib/ssr.js CHANGED
@@ -1,64 +1,7 @@
1
- import { createContext, h, pushContext } from "@pyreon/core";
1
+ import { h, pushContext } from "@pyreon/core";
2
2
  import { renderToString } from "@pyreon/runtime-server";
3
+ import { HeadContext, createHeadContext } from "@pyreon/head/context";
3
4
 
4
- //#region src/context.ts
5
- function createHeadContext() {
6
- const map = /* @__PURE__ */ new Map();
7
- let dirty = true;
8
- let cachedTags = [];
9
- let cachedTitleTemplate;
10
- let cachedHtmlAttrs = {};
11
- let cachedBodyAttrs = {};
12
- function rebuild() {
13
- if (!dirty) return;
14
- dirty = false;
15
- const keyed = /* @__PURE__ */ new Map();
16
- const unkeyed = [];
17
- let titleTemplate;
18
- const htmlAttrs = {};
19
- const bodyAttrs = {};
20
- for (const entry of map.values()) {
21
- for (const tag of entry.tags) if (tag.key) keyed.set(tag.key, tag);
22
- else unkeyed.push(tag);
23
- if (entry.titleTemplate !== void 0) titleTemplate = entry.titleTemplate;
24
- if (entry.htmlAttrs) Object.assign(htmlAttrs, entry.htmlAttrs);
25
- if (entry.bodyAttrs) Object.assign(bodyAttrs, entry.bodyAttrs);
26
- }
27
- cachedTags = [...keyed.values(), ...unkeyed];
28
- cachedTitleTemplate = titleTemplate;
29
- cachedHtmlAttrs = htmlAttrs;
30
- cachedBodyAttrs = bodyAttrs;
31
- }
32
- return {
33
- add(id, entry) {
34
- map.set(id, entry);
35
- dirty = true;
36
- },
37
- remove(id) {
38
- map.delete(id);
39
- dirty = true;
40
- },
41
- resolve() {
42
- rebuild();
43
- return cachedTags;
44
- },
45
- resolveTitleTemplate() {
46
- rebuild();
47
- return cachedTitleTemplate;
48
- },
49
- resolveHtmlAttrs() {
50
- rebuild();
51
- return cachedHtmlAttrs;
52
- },
53
- resolveBodyAttrs() {
54
- rebuild();
55
- return cachedBodyAttrs;
56
- }
57
- };
58
- }
59
- const HeadContext = createContext(null);
60
-
61
- //#endregion
62
5
  //#region src/ssr.ts
63
6
  const VOID_TAGS = new Set([
64
7
  "meta",
@@ -0,0 +1,205 @@
1
+ //#region src/context.d.ts
2
+ interface HeadTag {
3
+ /** HTML tag name */
4
+ tag: 'title' | 'meta' | 'link' | 'script' | 'style' | 'base' | 'noscript';
5
+ /**
6
+ * Deduplication key. Tags with the same key replace each other;
7
+ * innermost component (last added) wins.
8
+ * Example: all components setting the page title use key "title".
9
+ */
10
+ key?: string;
11
+ /** HTML attributes for the tag */
12
+ props?: Record<string, string>;
13
+ /** Text content — for <title>, <script>, <style>, <noscript> */
14
+ children?: string;
15
+ }
16
+ /** Standard `<meta>` tag attributes. Catches typos like `{ naem: "description" }`. */
17
+ interface MetaTag {
18
+ /** Standard meta name (e.g. "description", "viewport", "robots") */
19
+ name?: string;
20
+ /** Open Graph / social property (e.g. "og:title", "twitter:card") */
21
+ property?: string;
22
+ /** HTTP equivalent header (e.g. "refresh", "content-type") */
23
+ 'http-equiv'?: string;
24
+ /** Value associated with name, property, or http-equiv */
25
+ content?: string;
26
+ /** Document character encoding (e.g. "utf-8") */
27
+ charset?: string;
28
+ /** Schema.org itemprop */
29
+ itemprop?: string;
30
+ /** Media condition for applicability (e.g. "(prefers-color-scheme: dark)") */
31
+ media?: string;
32
+ }
33
+ /** Standard `<link>` tag attributes. */
34
+ interface LinkTag {
35
+ /** Relationship to the current document (e.g. "stylesheet", "icon", "canonical") */
36
+ rel?: string;
37
+ /** URL of the linked resource */
38
+ href?: string;
39
+ /** Resource type hint for preloading (e.g. "style", "script", "font") */
40
+ as?: string;
41
+ /** MIME type (e.g. "text/css", "image/png") */
42
+ type?: string;
43
+ /** Media query for conditional loading */
44
+ media?: string;
45
+ /** CORS mode */
46
+ crossorigin?: string;
47
+ /** Subresource integrity hash */
48
+ integrity?: string;
49
+ /** Icon sizes (e.g. "32x32", "any") */
50
+ sizes?: string;
51
+ /** Language of the linked resource */
52
+ hreflang?: string;
53
+ /** Title for the link (used for alternate stylesheets) */
54
+ title?: string;
55
+ /** Fetch priority hint */
56
+ fetchpriority?: 'high' | 'low' | 'auto';
57
+ /** Referrer policy */
58
+ referrerpolicy?: string;
59
+ /** Image source set for preloading responsive images */
60
+ imagesrcset?: string;
61
+ /** Image sizes for preloading responsive images */
62
+ imagesizes?: string;
63
+ /** Disable the resource (for stylesheets) */
64
+ disabled?: string;
65
+ /** Color for mask-icon */
66
+ color?: string;
67
+ }
68
+ /** Standard `<script>` tag attributes. */
69
+ interface ScriptTag {
70
+ /** External script URL */
71
+ src?: string;
72
+ /** Script MIME type or module type (e.g. "module", "importmap") */
73
+ type?: string;
74
+ /** Load asynchronously */
75
+ async?: string;
76
+ /** Defer execution until document is parsed */
77
+ defer?: string;
78
+ /** CORS mode */
79
+ crossorigin?: string;
80
+ /** Subresource integrity hash */
81
+ integrity?: string;
82
+ /** Exclude from module-supporting browsers */
83
+ nomodule?: string;
84
+ /** Referrer policy */
85
+ referrerpolicy?: string;
86
+ /** Fetch priority hint */
87
+ fetchpriority?: string;
88
+ /** Inline script content */
89
+ children?: string;
90
+ }
91
+ /** Standard `<style>` tag attributes. */
92
+ interface StyleTag {
93
+ /** Inline CSS content (required) */
94
+ children: string;
95
+ /** Media query for conditional styles */
96
+ media?: string;
97
+ /** Nonce for CSP */
98
+ nonce?: string;
99
+ /** Title for alternate stylesheets */
100
+ title?: string;
101
+ /** Render-blocking behavior */
102
+ blocking?: string;
103
+ }
104
+ /**
105
+ * How eagerly the browser should act on a speculation rule.
106
+ * Per the W3C Speculation Rules spec.
107
+ */
108
+ type SpeculationEagerness = 'immediate' | 'eager' | 'moderate' | 'conservative';
109
+ /**
110
+ * A single speculation rule (one entry in a `prefetch` / `prerender` list).
111
+ *
112
+ * - `source: 'list'` + `urls` — prefetch/prerender these explicit URLs.
113
+ * - `source: 'document'` + `where` — let the browser pick links from the
114
+ * current document that match the predicate (e.g. a CSS selector via
115
+ * `{ selector_matches: '.router-link' }`).
116
+ */
117
+ interface SpeculationRule {
118
+ /** `'list'` (explicit `urls`) or `'document'` (predicate-driven). */
119
+ source?: 'list' | 'document';
120
+ /** Same-origin URLs to prefetch/prerender (for `source: 'list'`). */
121
+ urls?: string[];
122
+ /** Document predicate (for `source: 'document'`) — e.g. `{ selector_matches: 'a.next' }`. */
123
+ where?: Record<string, unknown>;
124
+ /** When the browser should fetch — defaults to the browser's per-source default. */
125
+ eagerness?: SpeculationEagerness;
126
+ /** Capability requirements, e.g. `['anonymous-client-ip-when-cross-origin']`. */
127
+ requires?: string[];
128
+ /** Referrer policy for the speculative request. */
129
+ referrer_policy?: string;
130
+ }
131
+ /**
132
+ * Declarative Speculation Rules — emitted as a single
133
+ * `<script type="speculationrules">` tag. Supported browsers prefetch or
134
+ * fully prerender the next document(s) so navigation is instant. Inert in
135
+ * non-supporting browsers (no polyfill needed). Opt-in: only emitted when
136
+ * `useHead({ speculationRules })` is called.
137
+ *
138
+ * @see https://developer.mozilla.org/docs/Web/API/Speculation_Rules_API
139
+ */
140
+ interface SpeculationRules {
141
+ /** Lightweight: fetch the response, no rendering. */
142
+ prefetch?: SpeculationRule[];
143
+ /** Heavy: fully render the next document in the background. */
144
+ prerender?: SpeculationRule[];
145
+ }
146
+ /** Standard `<base>` tag attributes. */
147
+ interface BaseTag {
148
+ /** Base URL for relative URLs in the document */
149
+ href?: string;
150
+ /** Default target for links and forms */
151
+ target?: '_blank' | '_self' | '_parent' | '_top';
152
+ }
153
+ interface UseHeadInput {
154
+ title?: string;
155
+ /**
156
+ * Title template — use `%s` as a placeholder for the page title.
157
+ * Applied to the resolved title after deduplication.
158
+ * @example useHead({ titleTemplate: "%s | My App" })
159
+ */
160
+ titleTemplate?: string | ((title: string) => string);
161
+ meta?: MetaTag[];
162
+ link?: LinkTag[];
163
+ script?: ScriptTag[];
164
+ style?: StyleTag[];
165
+ noscript?: {
166
+ children: string;
167
+ }[];
168
+ /** Convenience: emits a <script type="application/ld+json"> tag with JSON.stringify'd content */
169
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
170
+ /**
171
+ * Convenience: emits a `<script type="speculationrules">` tag with the
172
+ * JSON.stringify'd rules. Supported browsers prefetch/prerender the next
173
+ * document(s) for near-instant navigation; inert elsewhere. Opt-in.
174
+ * @example useHead({ speculationRules: { prerender: [{ source: 'list', urls: ['/about'], eagerness: 'moderate' }] } })
175
+ */
176
+ speculationRules?: SpeculationRules;
177
+ base?: BaseTag;
178
+ /** Attributes to set on the <html> element (e.g. { lang: "en", dir: "ltr" }) */
179
+ htmlAttrs?: Record<string, string>;
180
+ /** Attributes to set on the <body> element (e.g. { class: "dark" }) */
181
+ bodyAttrs?: Record<string, string>;
182
+ }
183
+ interface HeadEntry {
184
+ tags: HeadTag[];
185
+ titleTemplate?: string | ((title: string) => string) | undefined;
186
+ htmlAttrs?: Record<string, string> | undefined;
187
+ bodyAttrs?: Record<string, string> | undefined;
188
+ }
189
+ interface HeadContextValue {
190
+ add(id: symbol, entry: HeadEntry): void;
191
+ remove(id: symbol): void;
192
+ /** Returns deduplicated tags — last-added entry wins per key */
193
+ resolve(): HeadTag[];
194
+ /** Returns the merged titleTemplate (last-added wins) */
195
+ resolveTitleTemplate(): (string | ((title: string) => string)) | undefined;
196
+ /** Returns merged htmlAttrs (later entries override earlier) */
197
+ resolveHtmlAttrs(): Record<string, string>;
198
+ /** Returns merged bodyAttrs (later entries override earlier) */
199
+ resolveBodyAttrs(): Record<string, string>;
200
+ }
201
+ declare function createHeadContext(): HeadContextValue;
202
+ declare const HeadContext: import("@pyreon/core").Context<HeadContextValue | null>;
203
+ //#endregion
204
+ export { BaseTag, HeadContext, HeadContextValue, HeadEntry, HeadTag, LinkTag, MetaTag, ScriptTag, SpeculationEagerness, SpeculationRule, SpeculationRules, StyleTag, UseHeadInput, createHeadContext };
205
+ //# sourceMappingURL=context2.d.ts.map