@pajecawav/yamf 0.0.9 → 0.0.11
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/dist/hooks/useHead.d.ts +0 -11
- package/dist/hooks/useHead.d.ts.map +1 -1
- package/dist/hooks/useHead.js +2 -11
- package/dist/hooks/useHead.js.map +1 -1
- package/dist/island/client.js +4 -1
- package/dist/island/client.js.map +1 -1
- package/dist/island/head.js +27 -0
- package/dist/island/head.js.map +1 -0
- package/dist/server/entry.mjs +1 -1
- package/dist/server/entry.mjs.map +1 -1
- package/package.json +12 -12
- package/src/hooks/useHead.tsx +3 -38
- package/src/island/client.tsx +11 -1
- package/src/island/head.ts +56 -0
- package/src/server/entry.tsx +2 -2
package/dist/hooks/useHead.d.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { YamfHead } from "../shared/head.js";
|
|
2
|
-
import { ClientUnhead } from "unhead/client";
|
|
3
2
|
import { UseSeoMetaInput } from "unhead/types";
|
|
4
3
|
//#region src/hooks/useHead.d.ts
|
|
5
|
-
interface StreamQueue {
|
|
6
|
-
_q?: unknown[][];
|
|
7
|
-
push?: (batch: unknown[]) => void;
|
|
8
|
-
}
|
|
9
|
-
declare global {
|
|
10
|
-
interface Window {
|
|
11
|
-
__unhead__?: StreamQueue;
|
|
12
|
-
__yamfHead__?: ClientUnhead;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
4
|
export declare const useHead: (input?: YamfHead) => void;
|
|
16
5
|
export declare const useSeoMeta: (input?: UseSeoMetaInput) => void;
|
|
17
6
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHead.d.ts","names":[],"sources":["../../src/hooks/useHead.tsx"],"mappings":"
|
|
1
|
+
{"version":3,"file":"useHead.d.ts","names":[],"sources":["../../src/hooks/useHead.tsx"],"mappings":";;;qBAkDa,UAAW,QAAQ;qBA0BnB,aAAc,QAAQ"}
|
package/dist/hooks/useHead.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { useSSRContext } from "../context/ssr.js";
|
|
2
|
+
import { ensureClientHead } from "../island/head.js";
|
|
2
3
|
import { useEffect, useRef } from "hono/jsx";
|
|
3
4
|
import { useHead, useSeoMeta } from "unhead";
|
|
4
|
-
import { createHead } from "unhead/client";
|
|
5
5
|
//#region src/hooks/useHead.tsx
|
|
6
|
-
if (!import.meta.env.SSR)
|
|
7
|
-
const head = createHead();
|
|
8
|
-
const drain = (batch) => {
|
|
9
|
-
for (const input of batch) head.push(input);
|
|
10
|
-
};
|
|
11
|
-
const queue = window.__unhead__?._q;
|
|
12
|
-
if (queue) for (const batch of queue) drain(batch);
|
|
13
|
-
window.__unhead__ = { push: drain };
|
|
14
|
-
window.__yamfHead__ = head;
|
|
15
|
-
}
|
|
6
|
+
if (!import.meta.env.SSR) ensureClientHead();
|
|
16
7
|
/**
|
|
17
8
|
* Binds an unhead entry to the calling component's lifecycle: the entry is
|
|
18
9
|
* created once on mount, patched on every re-render with the latest input,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHead.js","names":["useHead","_useHead","useSeoMeta","_useSeoMeta"],"sources":["../../src/hooks/useHead.tsx"],"sourcesContent":["import { useEffect, useRef } from \"hono/jsx\";\nimport { useHead as _useHead, useSeoMeta as _useSeoMeta } from \"unhead\";\nimport type { ClientUnhead } from \"unhead/client\";\nimport
|
|
1
|
+
{"version":3,"file":"useHead.js","names":["useHead","_useHead","useSeoMeta","_useSeoMeta"],"sources":["../../src/hooks/useHead.tsx"],"sourcesContent":["import { useEffect, useRef } from \"hono/jsx\";\nimport { useHead as _useHead, useSeoMeta as _useSeoMeta } from \"unhead\";\nimport type { ClientUnhead } from \"unhead/client\";\nimport type { ActiveHeadEntry, UseSeoMetaInput } from \"unhead/types\";\nimport { useSSRContext } from \"#/context/ssr\";\nimport { ensureClientHead } from \"#/island/head\";\nimport type { YamfHead } from \"#/shared/head\";\n\nif (!import.meta.env.SSR) {\n\tensureClientHead();\n}\n\n/**\n * Binds an unhead entry to the calling component's lifecycle: the entry is\n * created once on mount, patched on every re-render with the latest input,\n * and disposed on unmount. Without this, every re-render pushes a new entry\n * that is never removed — a leak and growing DOM render cost.\n */\nconst useHeadEntryLifecycle = <T,>(\n\tinput: T | undefined,\n\tcreateEntry: (head: ClientUnhead, input: T) => ActiveHeadEntry<T>,\n): void => {\n\tconst entryRef = useRef<ActiveHeadEntry<T> | null>(null);\n\n\tuseEffect(() => {\n\t\tconst head = window.__yamfHead__;\n\n\t\tif (!head || input === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst entry = createEntry(head, input);\n\n\t\tentryRef.current = entry;\n\n\t\treturn () => {\n\t\t\tentry.dispose();\n\t\t\tentryRef.current = null;\n\t\t};\n\t\t// mount-only: the entry is created once per component instance\n\t\t// oxlint-disable-next-line eslint(exhaustive-deps)\n\t}, []);\n\n\tuseEffect(() => {\n\t\tif (input !== undefined) {\n\t\t\tentryRef.current?.patch(input);\n\t\t}\n\t}, [input]);\n};\n\nexport const useHead = (input?: YamfHead): void => {\n\tif (import.meta.env.SSR) {\n\t\tif (input === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst ctx = useSSRContext();\n\n\t\tif (ctx?.head) {\n\t\t\t// the custom `seo` key is yamf sugar for useSeoMeta — process it\n\t\t\t// instead of passing an unknown key down to unhead\n\t\t\tconst { seo, ...rest } = input;\n\n\t\t\tif (seo !== undefined) {\n\t\t\t\t_useSeoMeta(ctx.head, seo);\n\t\t\t}\n\n\t\t\t_useHead(ctx.head, rest);\n\t\t}\n\n\t\treturn;\n\t}\n\n\tuseHeadEntryLifecycle(input, (head, resolved) => _useHead(head, resolved));\n};\n\nexport const useSeoMeta = (input?: UseSeoMetaInput): void => {\n\tif (import.meta.env.SSR) {\n\t\tif (input === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst ctx = useSSRContext();\n\n\t\tif (ctx?.head) {\n\t\t\t_useSeoMeta(ctx.head, input);\n\t\t}\n\n\t\treturn;\n\t}\n\n\tuseHeadEntryLifecycle(input, (head, resolved) => _useSeoMeta(head, resolved));\n};\n"],"mappings":";;;;;AAQA,IAAI,CAAC,YAAY,IAAI,KACpB,iBAAiB;;;;;;;AASlB,MAAM,yBACL,OACA,gBACU;CACV,MAAM,WAAW,OAAkC,IAAI;CAEvD,gBAAgB;EACf,MAAM,OAAO,OAAO;EAEpB,IAAI,CAAC,QAAQ,UAAU,KAAA,GACtB;EAGD,MAAM,QAAQ,YAAY,MAAM,KAAK;EAErC,SAAS,UAAU;EAEnB,aAAa;GACZ,MAAM,QAAQ;GACd,SAAS,UAAU;EACpB;CAGD,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACf,IAAI,UAAU,KAAA,GACb,SAAS,SAAS,MAAM,KAAK;CAE/B,GAAG,CAAC,KAAK,CAAC;AACX;AAEA,MAAaA,aAAW,UAA2B;CAClD,IAAI,YAAY,IAAI,KAAK;EACxB,IAAI,UAAU,KAAA,GACb;EAGD,MAAM,MAAM,cAAc;EAE1B,IAAI,KAAK,MAAM;GAGd,MAAM,EAAE,KAAK,GAAG,SAAS;GAEzB,IAAI,QAAQ,KAAA,GACX,WAAY,IAAI,MAAM,GAAG;GAG1B,QAAS,IAAI,MAAM,IAAI;EACxB;EAEA;CACD;CAEA,sBAAsB,QAAQ,MAAM,aAAaC,QAAS,MAAM,QAAQ,CAAC;AAC1E;AAEA,MAAaC,gBAAc,UAAkC;CAC5D,IAAI,YAAY,IAAI,KAAK;EACxB,IAAI,UAAU,KAAA,GACb;EAGD,MAAM,MAAM,cAAc;EAE1B,IAAI,KAAK,MACR,WAAY,IAAI,MAAM,KAAK;EAG5B;CACD;CAEA,sBAAsB,QAAQ,MAAM,aAAaC,WAAY,MAAM,QAAQ,CAAC;AAC7E"}
|
package/dist/island/client.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { ensureClientHead } from "./head.js";
|
|
1
2
|
import { jsx } from "hono/jsx/jsx-runtime";
|
|
2
3
|
import { parse } from "devalue";
|
|
3
4
|
import { hydrateRoot } from "hono/jsx/dom/client";
|
|
4
5
|
import { withLeadingSlash } from "ufo";
|
|
5
6
|
//#region src/island/client.tsx
|
|
7
|
+
ensureClientHead();
|
|
6
8
|
const listeners = /* @__PURE__ */ new WeakMap();
|
|
7
9
|
const observer = new IntersectionObserver((entries) => {
|
|
8
10
|
for (const entry of entries) if (entry.isIntersecting) {
|
|
@@ -31,7 +33,8 @@ const requestIdle = (callback) => {
|
|
|
31
33
|
* element (src/client/index.ts) once this runtime module has been loaded.
|
|
32
34
|
*/
|
|
33
35
|
const hydrateIsland = (island) => {
|
|
34
|
-
const
|
|
36
|
+
const rawProps = island.getAttribute("island-props");
|
|
37
|
+
const islandProps = rawProps === null ? [] : parse(rawProps);
|
|
35
38
|
const islandSrc = island.getAttribute("island-src");
|
|
36
39
|
const islandEntry = island.getAttribute("island-entry");
|
|
37
40
|
const islandClient = island.getAttribute("island-client") ?? "load";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../src/island/client.tsx"],"sourcesContent":["import { parse } from \"devalue\";\nimport type { FC } from \"hono/jsx\";\nimport { hydrateRoot } from \"hono/jsx/dom/client\";\nimport { withLeadingSlash } from \"ufo\";\nimport type { IslandClientDirectiveSerialized } from \"./types\";\n\ndeclare let __yamf_raw_import__: <T>(file: string) => Promise<T>;\n\nconst listeners: WeakMap<Element, VoidFunction> = new WeakMap();\n\nconst observer: IntersectionObserver = new IntersectionObserver(entries => {\n\tfor (const entry of entries) {\n\t\tif (entry.isIntersecting) {\n\t\t\tlisteners.get(entry.target)?.();\n\t\t\tunobserve(entry.target);\n\t\t}\n\t}\n});\n\nconst observe = (target: Element, cb: VoidFunction) => {\n\tobserver.observe(target);\n\tlisteners.set(target, cb);\n};\n\nconst unobserve = (target: Element) => {\n\tobserver.unobserve(target);\n\tlisteners.delete(target);\n};\n\n// Safari does not enable requestIdleCallback by default in any stable\n// release — without a fallback, client:idle islands never hydrate there\nconst requestIdle = (callback: () => void): void => {\n\tconst ric = window.requestIdleCallback;\n\n\tif (typeof ric === \"function\") {\n\t\tric(callback);\n\t\treturn;\n\t}\n\n\tsetTimeout(callback, 1);\n};\n\n/**\n * Hydrates a single <yamf-island> island. Called by the bootstrap custom\n * element (src/client/index.ts) once this runtime module has been loaded.\n */\nexport const hydrateIsland = (island: HTMLElement): void => {\n\tconst
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../src/island/client.tsx"],"sourcesContent":["import { parse } from \"devalue\";\nimport type { FC } from \"hono/jsx\";\nimport { hydrateRoot } from \"hono/jsx/dom/client\";\nimport { withLeadingSlash } from \"ufo\";\nimport { ensureClientHead } from \"./head\";\nimport type { IslandClientDirectiveSerialized } from \"./types\";\n\ndeclare let __yamf_raw_import__: <T>(file: string) => Promise<T>;\n\n// this runtime loads whenever a page contains islands — set up the client\n// head here so the handshake payload and streamed head patches actually\n// apply, even when nothing imports the useHead hook\nensureClientHead();\n\nconst listeners: WeakMap<Element, VoidFunction> = new WeakMap();\n\nconst observer: IntersectionObserver = new IntersectionObserver(entries => {\n\tfor (const entry of entries) {\n\t\tif (entry.isIntersecting) {\n\t\t\tlisteners.get(entry.target)?.();\n\t\t\tunobserve(entry.target);\n\t\t}\n\t}\n});\n\nconst observe = (target: Element, cb: VoidFunction) => {\n\tobserver.observe(target);\n\tlisteners.set(target, cb);\n};\n\nconst unobserve = (target: Element) => {\n\tobserver.unobserve(target);\n\tlisteners.delete(target);\n};\n\n// Safari does not enable requestIdleCallback by default in any stable\n// release — without a fallback, client:idle islands never hydrate there\nconst requestIdle = (callback: () => void): void => {\n\tconst ric = window.requestIdleCallback;\n\n\tif (typeof ric === \"function\") {\n\t\tric(callback);\n\t\treturn;\n\t}\n\n\tsetTimeout(callback, 1);\n};\n\n/**\n * Hydrates a single <yamf-island> island. Called by the bootstrap custom\n * element (src/client/index.ts) once this runtime module has been loaded.\n */\nexport const hydrateIsland = (island: HTMLElement): void => {\n\t// skip islands omit the attribute entirely — parse after (and only if) it\n\t// exists, otherwise parse(\"{}\") would throw \"Invalid input\" and abort the\n\t// whole pending-hydration loop\n\tconst rawProps = island.getAttribute(\"island-props\");\n\tconst islandProps = rawProps === null ? [] : parse(rawProps);\n\tconst islandSrc = island.getAttribute(\"island-src\");\n\tconst islandEntry = island.getAttribute(\"island-entry\");\n\t// oxlint-disable-next-line typescript/no-unsafe-type-assertion\n\tconst islandClient = (island.getAttribute(\"island-client\") ??\n\t\t\"load\") as IslandClientDirectiveSerialized;\n\n\tif (!islandSrc) {\n\t\tthrow new Error(\"Missing island-src attribute\");\n\t}\n\n\tif (!islandEntry) {\n\t\tthrow new Error(\"Missing island-entry attribute\");\n\t}\n\n\tconst hydrate = (mod: Record<string, FC>) => {\n\t\tconst Comp = mod[islandEntry];\n\n\t\tif (!Comp) {\n\t\t\tthrow new Error(`Missing island entry ${islandEntry} in ${islandSrc}`);\n\t\t}\n\n\t\thydrateRoot(island, <Comp {...islandProps} />);\n\t};\n\n\tconst initIsland = () => {\n\t\tconst src = withLeadingSlash(islandSrc);\n\t\tvoid __yamf_raw_import__<Record<string, FC>>(src).then(hydrate);\n\t};\n\n\tswitch (islandClient) {\n\t\tcase \"true\":\n\t\tcase \"load\":\n\t\t\tinitIsland();\n\t\t\tbreak;\n\t\tcase \"idle\":\n\t\t\trequestIdle(initIsland);\n\t\t\tbreak;\n\t\tcase \"visible\":\n\t\t\t// yamf-island has `display: contents` which breaks IntersectionObserver\n\t\t\t// so we have to observe the first child instead if it exists\n\t\t\tif (island.firstElementChild) {\n\t\t\t\tobserve(island.firstElementChild, initIsland);\n\t\t\t} else {\n\t\t\t\tinitIsland();\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"false\":\n\t\tcase \"skip\":\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tislandClient satisfies never;\n\t\t\t// oxlint-disable-next-line typescript/restrict-template-expressions\n\t\t\tthrow new Error(`Invalid island-client value: ${islandClient}`);\n\t}\n};\n"],"mappings":";;;;;;AAYA,iBAAiB;AAEjB,MAAM,4BAA4C,IAAI,QAAQ;AAE9D,MAAM,WAAiC,IAAI,sBAAqB,YAAW;CAC1E,KAAK,MAAM,SAAS,SACnB,IAAI,MAAM,gBAAgB;EACzB,UAAU,IAAI,MAAM,MAAM,CAAC,GAAG;EAC9B,UAAU,MAAM,MAAM;CACvB;AAEF,CAAC;AAED,MAAM,WAAW,QAAiB,OAAqB;CACtD,SAAS,QAAQ,MAAM;CACvB,UAAU,IAAI,QAAQ,EAAE;AACzB;AAEA,MAAM,aAAa,WAAoB;CACtC,SAAS,UAAU,MAAM;CACzB,UAAU,OAAO,MAAM;AACxB;AAIA,MAAM,eAAe,aAA+B;CACnD,MAAM,MAAM,OAAO;CAEnB,IAAI,OAAO,QAAQ,YAAY;EAC9B,IAAI,QAAQ;EACZ;CACD;CAEA,WAAW,UAAU,CAAC;AACvB;;;;;AAMA,MAAa,iBAAiB,WAA8B;CAI3D,MAAM,WAAW,OAAO,aAAa,cAAc;CACnD,MAAM,cAAc,aAAa,OAAO,CAAC,IAAI,MAAM,QAAQ;CAC3D,MAAM,YAAY,OAAO,aAAa,YAAY;CAClD,MAAM,cAAc,OAAO,aAAa,cAAc;CAEtD,MAAM,eAAgB,OAAO,aAAa,eAAe,KACxD;CAED,IAAI,CAAC,WACJ,MAAM,IAAI,MAAM,8BAA8B;CAG/C,IAAI,CAAC,aACJ,MAAM,IAAI,MAAM,gCAAgC;CAGjD,MAAM,WAAW,QAA4B;EAC5C,MAAM,OAAO,IAAI;EAEjB,IAAI,CAAC,MACJ,MAAM,IAAI,MAAM,wBAAwB,YAAY,MAAM,WAAW;EAGtE,YAAY,QAAQ,oBAAC,MAAD,EAAM,GAAI,YAAc,CAAA,CAAC;CAC9C;CAEA,MAAM,mBAAmB;EACxB,MAAM,MAAM,iBAAiB,SAAS;EACtC,oBAA6C,GAAG,CAAC,CAAC,KAAK,OAAO;CAC/D;CAEA,QAAQ,cAAR;EACC,KAAK;EACL,KAAK;GACJ,WAAW;GACX;EACD,KAAK;GACJ,YAAY,UAAU;GACtB;EACD,KAAK;GAGJ,IAAI,OAAO,mBACV,QAAQ,OAAO,mBAAmB,UAAU;QAE5C,WAAW;GAEZ;EACD,KAAK;EACL,KAAK,QACJ;EACD,SAGC,MAAM,IAAI,MAAM,gCAAgC,cAAc;CAChE;AACD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHead } from "unhead/client";
|
|
2
|
+
//#region src/island/head.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates the client head, drains everything the server streamed into
|
|
5
|
+
* `window.__unhead__` (the handshake payload and streamed suspense patches)
|
|
6
|
+
* and upgrades the stub so late patches from an open stream keep applying.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: it is called from the island hydration runtime (which loads
|
|
9
|
+
* whenever a page contains islands) and from the `useHead` hook module —
|
|
10
|
+
* whichever runs first wins.
|
|
11
|
+
*/
|
|
12
|
+
const ensureClientHead = () => {
|
|
13
|
+
if (window.__yamfHead__) return window.__yamfHead__;
|
|
14
|
+
const head = createHead();
|
|
15
|
+
const drain = (batch) => {
|
|
16
|
+
for (const input of batch) head.push(input);
|
|
17
|
+
};
|
|
18
|
+
const queue = window.__unhead__?._q;
|
|
19
|
+
if (queue) for (const batch of queue) drain(batch);
|
|
20
|
+
window.__unhead__ = { push: drain };
|
|
21
|
+
window.__yamfHead__ = head;
|
|
22
|
+
return head;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ensureClientHead };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=head.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"head.js","names":[],"sources":["../../src/island/head.ts"],"sourcesContent":["import { createHead } from \"unhead/client\";\nimport type { ClientUnhead } from \"unhead/client\";\nimport type { ResolvableHead } from \"unhead/types\";\n\ninterface StreamQueue {\n\t_q?: unknown[][];\n\tpush?: (batch: unknown[]) => void;\n}\n\ndeclare global {\n\tinterface Window {\n\t\t__unhead__?: StreamQueue;\n\t\t__yamfHead__?: ClientUnhead;\n\t}\n}\n\n/**\n * Creates the client head, drains everything the server streamed into\n * `window.__unhead__` (the handshake payload and streamed suspense patches)\n * and upgrades the stub so late patches from an open stream keep applying.\n *\n * Idempotent: it is called from the island hydration runtime (which loads\n * whenever a page contains islands) and from the `useHead` hook module —\n * whichever runs first wins.\n */\nexport const ensureClientHead = (): ClientUnhead => {\n\tif (window.__yamfHead__) {\n\t\treturn window.__yamfHead__;\n\t}\n\n\tconst head = createHead();\n\n\tconst drain = (batch: unknown[]): void => {\n\t\tfor (const input of batch) {\n\t\t\thead.push(input as ResolvableHead);\n\t\t}\n\t};\n\n\t// inline scripts (the handshake payload and streamed suspense patches)\n\t// queue into window.__unhead__ while the document parses; drain them into\n\t// the client head\n\tconst queue = window.__unhead__?._q;\n\n\tif (queue) {\n\t\tfor (const batch of queue) {\n\t\t\tdrain(batch);\n\t\t}\n\t}\n\n\t// keep late patches (a stream that is still open) working\n\twindow.__unhead__ = { push: drain };\n\n\twindow.__yamfHead__ = head;\n\n\treturn head;\n};\n"],"mappings":";;;;;;;;;;;AAyBA,MAAa,yBAAuC;CACnD,IAAI,OAAO,cACV,OAAO,OAAO;CAGf,MAAM,OAAO,WAAW;CAExB,MAAM,SAAS,UAA2B;EACzC,KAAK,MAAM,SAAS,OACnB,KAAK,KAAK,KAAuB;CAEnC;CAKA,MAAM,QAAQ,OAAO,YAAY;CAEjC,IAAI,OACH,KAAK,MAAM,SAAS,OACnB,MAAM,KAAK;CAKb,OAAO,aAAa,EAAE,MAAM,MAAM;CAElC,OAAO,eAAe;CAEtB,OAAO;AACR"}
|
package/dist/server/entry.mjs
CHANGED
|
@@ -13,7 +13,7 @@ for (const [relativePath, handler] of Object.entries(pages).toSorted((a, b) => a
|
|
|
13
13
|
const ext = path.extname(relativePath);
|
|
14
14
|
let route = path.relative("/src/pages", relativePath);
|
|
15
15
|
if (ext.length) route = route.slice(0, -ext.length);
|
|
16
|
-
route = route.replace(/\.[A-Za-z]+$/, "").replace(/\(([^(/\\]+)\)[/\\]/g, "").replace(/\[\.{3}]/g, "**").replace(/\[\.{3}([^\]]+)]/g, (_, p) => "**:" + p.replace(/[^\w
|
|
16
|
+
route = route.replace(/\.[A-Za-z]+$/, "").replace(/\(([^(/\\]+)\)[/\\]/g, "").replace(/\[\.{3}]/g, "**").replace(/\[\.{3}([^\]]+)]/g, (_, p) => "**:" + p.replace(/[^\w]/g, "_")).replace(/\[([^/\]]+)]/g, (_, p) => ":" + p.replace(/[^\w]/g, "_")).replace(/(\/|^)index$/, "") || "/";
|
|
17
17
|
route = withLeadingSlash(withoutTrailingSlash(route));
|
|
18
18
|
const conflictingFile = registeredRoutes.get(route);
|
|
19
19
|
if (conflictingFile && conflictingFile !== relativePath) console.warn(`[yamf] duplicate page route "${route}": ${conflictingFile} and ${relativePath} both map to it — the routes conflict`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry.mjs","names":["pagesServerAssets"],"sources":["../../src/server/entry.tsx"],"sourcesContent":["import path from \"node:path\";\nimport {\n\tdefineHandler,\n\tEventHandler,\n\tH3Event,\n\tHTTPError,\n\twriteEarlyHints,\n\ttype EventHandlerRequest,\n\ttype EventHandlerWithFetch,\n} from \"nitro/h3\";\nimport { addRoute, createRouter, findRoute } from \"rou3\";\nimport { withLeadingSlash, withoutTrailingSlash } from \"ufo\";\nimport { clientAssets } from \"virtual:yamf:assets\";\nimport { errorHandler } from \"virtual:yamf:error-handler\";\nimport { pages, assets as pagesServerAssets } from \"virtual:yamf:pages\";\nimport { rootAssets } from \"virtual:yamf:root\";\nimport type { PageHandler } from \"#/page\";\nimport { YamfHead } from \"#/shared/head\";\n\ninterface Route {\n\thandler: () => Promise<PageHandler>;\n\tserverAssets?: ImportAssetsResult;\n}\n\nconst router = createRouter<Route>();\n\nconst registeredRoutes = new Map<string, string>();\n\nfor (const [relativePath, handler] of Object.entries(pages).toSorted((a, b) =>\n\ta[0].localeCompare(b[0]),\n)) {\n\tconst ext = path.extname(relativePath);\n\n\tlet route = path.relative(\"/src/pages\", relativePath);\n\n\tif (ext.length) {\n\t\troute = route.slice(0, -ext.length);\n\t}\n\n\troute =\n\t\troute\n\t\t\t.replace(/\\.[A-Za-z]+$/, \"\")\n\t\t\t.replace(/\\(([^(/\\\\]+)\\)[/\\\\]/g, \"\")\n\t\t\t.replace(/\\[\\.{3}]/g, \"**\")\n\t\t\t.replace(/\\[\\.{3}([^\\]]+)]/g, (_, p: string) => \"**:\" + p.replace(/[^\\w
|
|
1
|
+
{"version":3,"file":"entry.mjs","names":["pagesServerAssets"],"sources":["../../src/server/entry.tsx"],"sourcesContent":["import path from \"node:path\";\nimport {\n\tdefineHandler,\n\tEventHandler,\n\tH3Event,\n\tHTTPError,\n\twriteEarlyHints,\n\ttype EventHandlerRequest,\n\ttype EventHandlerWithFetch,\n} from \"nitro/h3\";\nimport { addRoute, createRouter, findRoute } from \"rou3\";\nimport { withLeadingSlash, withoutTrailingSlash } from \"ufo\";\nimport { clientAssets } from \"virtual:yamf:assets\";\nimport { errorHandler } from \"virtual:yamf:error-handler\";\nimport { pages, assets as pagesServerAssets } from \"virtual:yamf:pages\";\nimport { rootAssets } from \"virtual:yamf:root\";\nimport type { PageHandler } from \"#/page\";\nimport { YamfHead } from \"#/shared/head\";\n\ninterface Route {\n\thandler: () => Promise<PageHandler>;\n\tserverAssets?: ImportAssetsResult;\n}\n\nconst router = createRouter<Route>();\n\nconst registeredRoutes = new Map<string, string>();\n\nfor (const [relativePath, handler] of Object.entries(pages).toSorted((a, b) =>\n\ta[0].localeCompare(b[0]),\n)) {\n\tconst ext = path.extname(relativePath);\n\n\tlet route = path.relative(\"/src/pages\", relativePath);\n\n\tif (ext.length) {\n\t\troute = route.slice(0, -ext.length);\n\t}\n\n\troute =\n\t\troute\n\t\t\t.replace(/\\.[A-Za-z]+$/, \"\")\n\t\t\t.replace(/\\(([^(/\\\\]+)\\)[/\\\\]/g, \"\")\n\t\t\t.replace(/\\[\\.{3}]/g, \"**\")\n\t\t\t.replace(/\\[\\.{3}([^\\]]+)]/g, (_, p: string) => \"**:\" + p.replace(/[^\\w]/g, \"_\"))\n\t\t\t.replace(/\\[([^/\\]]+)]/g, (_, p: string) => \":\" + p.replace(/[^\\w]/g, \"_\"))\n\t\t\t.replace(/(\\/|^)index$/, \"\") || \"/\";\n\n\troute = withLeadingSlash(withoutTrailingSlash(route));\n\n\tconst conflictingFile = registeredRoutes.get(route);\n\n\tif (conflictingFile && conflictingFile !== relativePath) {\n\t\tconsole.warn(\n\t\t\t`[yamf] duplicate page route \"${route}\": ${conflictingFile} and ${relativePath} both map to it — the routes conflict`,\n\t\t);\n\t}\n\n\tregisteredRoutes.set(route, relativePath);\n\n\taddRoute(router, \"GET\", route, {\n\t\thandler,\n\t\tserverAssets: pagesServerAssets[relativePath],\n\t});\n}\n\nexport type ServerEntry = EventHandlerWithFetch<EventHandlerRequest, Promise<unknown>>;\n\nexport interface DefineServerEntryOptions {\n\thead?: YamfHead | ((event: H3Event) => YamfHead);\n\tdisableEarlyHints?: boolean;\n\t/**\n\t * CSP nonce for the inline scripts yamf injects (head handshake payload\n\t * and streamed head patches).\n\t *\n\t * Note: unhead's stream bootstrap script (`window.__unhead__||(…)`) does\n\t * not support a nonce in unhead 3.x — with a strict `script-src` CSP it\n\t * must be allowed separately (e.g. via a hash).\n\t */\n\tnonce?: string | ((event: H3Event) => string | undefined);\n}\n\nexport const defineServerEntry = (options?: DefineServerEntryOptions): ServerEntry => {\n\tconst rootHandler: EventHandler<EventHandlerRequest, Promise<unknown>> = async event => {\n\t\tconst route = findRoute(router, \"GET\", event.url.pathname);\n\n\t\tif (!route) {\n\t\t\tthrow new HTTPError(\"Not found\", { status: 404 });\n\t\t}\n\n\t\tconst { handler, serverAssets } = route.data;\n\t\tevent.context.params = route.params;\n\n\t\tconst assets = clientAssets.merge(...[rootAssets, serverAssets].filter(x => !!x));\n\n\t\t// early hints are pointless inside the prerender worker — nitro's\n\t\t// prerenderer buffers the response to a file and discards them\n\t\tif (!options?.disableEarlyHints && !event.req.headers.has(\"x-nitro-prerender\")) {\n\t\t\tconst link = [\n\t\t\t\t...assets.js.map(script => `<${script.href}>; rel=modulepreload`),\n\t\t\t\t...assets.css.map(style => `<${style.href}>; rel=preload; as=style`),\n\t\t\t];\n\n\t\t\tif (link.length) {\n\t\t\t\tawait writeEarlyHints(event, { link });\n\t\t\t}\n\t\t}\n\n\t\treturn (await handler())(event, {\n\t\t\tassets,\n\t\t\thead: typeof options?.head === \"function\" ? options.head(event) : options?.head,\n\t\t\tnonce: typeof options?.nonce === \"function\" ? options.nonce(event) : options?.nonce,\n\t\t});\n\t};\n\n\treturn defineHandler(async event => {\n\t\tif (!errorHandler) {\n\t\t\treturn rootHandler(event);\n\t\t}\n\n\t\ttry {\n\t\t\treturn await rootHandler(event);\n\t\t} catch (error) {\n\t\t\tconst httpError = error instanceof HTTPError ? error : new HTTPError({ cause: error });\n\n\t\t\treturn errorHandler(httpError, event);\n\t\t}\n\t});\n};\n"],"mappings":";;;;;;;;;AAwBA,MAAM,SAAS,aAAoB;AAEnC,MAAM,mCAAmB,IAAI,IAAoB;AAEjD,KAAK,MAAM,CAAC,cAAc,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,UAAU,GAAG,MACxE,EAAE,EAAE,CAAC,cAAc,EAAE,EAAE,CACxB,GAAG;CACF,MAAM,MAAM,KAAK,QAAQ,YAAY;CAErC,IAAI,QAAQ,KAAK,SAAS,cAAc,YAAY;CAEpD,IAAI,IAAI,QACP,QAAQ,MAAM,MAAM,GAAG,CAAC,IAAI,MAAM;CAGnC,QACC,MACE,QAAQ,gBAAgB,EAAE,CAAC,CAC3B,QAAQ,wBAAwB,EAAE,CAAC,CACnC,QAAQ,aAAa,IAAI,CAAC,CAC1B,QAAQ,sBAAsB,GAAG,MAAc,QAAQ,EAAE,QAAQ,UAAU,GAAG,CAAC,CAAC,CAChF,QAAQ,kBAAkB,GAAG,MAAc,MAAM,EAAE,QAAQ,UAAU,GAAG,CAAC,CAAC,CAC1E,QAAQ,gBAAgB,EAAE,KAAK;CAElC,QAAQ,iBAAiB,qBAAqB,KAAK,CAAC;CAEpD,MAAM,kBAAkB,iBAAiB,IAAI,KAAK;CAElD,IAAI,mBAAmB,oBAAoB,cAC1C,QAAQ,KACP,gCAAgC,MAAM,KAAK,gBAAgB,OAAO,aAAa,sCAChF;CAGD,iBAAiB,IAAI,OAAO,YAAY;CAExC,SAAS,QAAQ,OAAO,OAAO;EAC9B;EACA,cAAcA,OAAkB;CACjC,CAAC;AACF;AAkBA,MAAa,qBAAqB,YAAoD;CACrF,MAAM,cAAmE,OAAM,UAAS;EACvF,MAAM,QAAQ,UAAU,QAAQ,OAAO,MAAM,IAAI,QAAQ;EAEzD,IAAI,CAAC,OACJ,MAAM,IAAI,UAAU,aAAa,EAAE,QAAQ,IAAI,CAAC;EAGjD,MAAM,EAAE,SAAS,iBAAiB,MAAM;EACxC,MAAM,QAAQ,SAAS,MAAM;EAE7B,MAAM,SAAS,aAAa,MAAM,GAAG,CAAC,YAAY,YAAY,CAAC,CAAC,QAAO,MAAK,CAAC,CAAC,CAAC,CAAC;EAIhF,IAAI,CAAC,SAAS,qBAAqB,CAAC,MAAM,IAAI,QAAQ,IAAI,mBAAmB,GAAG;GAC/E,MAAM,OAAO,CACZ,GAAG,OAAO,GAAG,KAAI,WAAU,IAAI,OAAO,KAAK,qBAAqB,GAChE,GAAG,OAAO,IAAI,KAAI,UAAS,IAAI,MAAM,KAAK,yBAAyB,CACpE;GAEA,IAAI,KAAK,QACR,MAAM,gBAAgB,OAAO,EAAE,KAAK,CAAC;EAEvC;EAEA,QAAQ,MAAM,QAAQ,EAAA,CAAG,OAAO;GAC/B;GACA,MAAM,OAAO,SAAS,SAAS,aAAa,QAAQ,KAAK,KAAK,IAAI,SAAS;GAC3E,OAAO,OAAO,SAAS,UAAU,aAAa,QAAQ,MAAM,KAAK,IAAI,SAAS;EAC/E,CAAC;CACF;CAEA,OAAO,cAAc,OAAM,UAAS;EACnC,IAAI,CAAC,cACJ,OAAO,YAAY,KAAK;EAGzB,IAAI;GACH,OAAO,MAAM,YAAY,KAAK;EAC/B,SAAS,OAAO;GACf,MAAM,YAAY,iBAAiB,YAAY,QAAQ,IAAI,UAAU,EAAE,OAAO,MAAM,CAAC;GAErF,OAAO,aAAa,WAAW,KAAK;EACrC;CACD,CAAC;AACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pajecawav/yamf",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.11",
|
|
5
5
|
"description": "Yet another meta framework",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/pajecawav/yamf#readme",
|
|
@@ -43,30 +43,30 @@
|
|
|
43
43
|
"@hono/react-compat": "^0.0.3",
|
|
44
44
|
"@standard-schema/spec": "^1.1.0",
|
|
45
45
|
"devalue": "^6.0.2",
|
|
46
|
-
"exsolve": "^1.1.
|
|
46
|
+
"exsolve": "^1.1.3",
|
|
47
47
|
"nitro": "3.0.260903-beta",
|
|
48
48
|
"rolldown-string": "^0.3.1",
|
|
49
|
-
"rou3": "^0.
|
|
49
|
+
"rou3": "^1.0.0",
|
|
50
50
|
"ufo": "^1.6.4",
|
|
51
|
-
"unhead": "^3.4.
|
|
52
|
-
"wouter": "^3.
|
|
51
|
+
"unhead": "^3.4.2",
|
|
52
|
+
"wouter": "^3.13.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@hono/react-compat": "^0.0.3",
|
|
56
|
-
"@pajecawav/tools": "^0.0
|
|
56
|
+
"@pajecawav/tools": "^0.1.0",
|
|
57
57
|
"@playwright/test": "^1.63.0",
|
|
58
|
-
"@types/node": "^26.6.
|
|
58
|
+
"@types/node": "^26.6.4",
|
|
59
59
|
"cross-env": "^10.1.0",
|
|
60
|
-
"hono": "^4.13.
|
|
60
|
+
"hono": "^4.13.12",
|
|
61
61
|
"husky": "^9.1.7",
|
|
62
62
|
"npm-run-all2": "^9.0.3",
|
|
63
|
-
"oxfmt": "^0.
|
|
64
|
-
"oxlint": "^1.
|
|
63
|
+
"oxfmt": "^0.71.0",
|
|
64
|
+
"oxlint": "^1.86.0",
|
|
65
65
|
"oxlint-tsgolint": "^7.0.2002",
|
|
66
|
-
"publint": "^0.3.
|
|
66
|
+
"publint": "^0.3.25",
|
|
67
67
|
"tsdown": "0.23.0",
|
|
68
68
|
"typescript": "~7.0.2",
|
|
69
|
-
"vite": "^8.3.
|
|
69
|
+
"vite": "^8.3.2"
|
|
70
70
|
},
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "tsdown",
|
package/src/hooks/useHead.tsx
CHANGED
|
@@ -1,48 +1,13 @@
|
|
|
1
1
|
import { useEffect, useRef } from "hono/jsx";
|
|
2
2
|
import { useHead as _useHead, useSeoMeta as _useSeoMeta } from "unhead";
|
|
3
3
|
import type { ClientUnhead } from "unhead/client";
|
|
4
|
-
import {
|
|
5
|
-
import type { ActiveHeadEntry, ResolvableHead, UseSeoMetaInput } from "unhead/types";
|
|
4
|
+
import type { ActiveHeadEntry, UseSeoMetaInput } from "unhead/types";
|
|
6
5
|
import { useSSRContext } from "#/context/ssr";
|
|
6
|
+
import { ensureClientHead } from "#/island/head";
|
|
7
7
|
import type { YamfHead } from "#/shared/head";
|
|
8
8
|
|
|
9
|
-
interface StreamQueue {
|
|
10
|
-
_q?: unknown[][];
|
|
11
|
-
push?: (batch: unknown[]) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
declare global {
|
|
15
|
-
interface Window {
|
|
16
|
-
__unhead__?: StreamQueue;
|
|
17
|
-
__yamfHead__?: ClientUnhead;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
9
|
if (!import.meta.env.SSR) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const drain = (batch: unknown[]): void => {
|
|
25
|
-
for (const input of batch) {
|
|
26
|
-
head.push(input as ResolvableHead);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// inline scripts (the handshake payload and streamed suspense patches)
|
|
31
|
-
// queue into window.__unhead__ while the document parses; this module is
|
|
32
|
-
// part of the deferred client entry and runs after parsing, so by now the
|
|
33
|
-
// queue holds everything the server sent — drain it into the client head
|
|
34
|
-
const queue = window.__unhead__?._q;
|
|
35
|
-
|
|
36
|
-
if (queue) {
|
|
37
|
-
for (const batch of queue) {
|
|
38
|
-
drain(batch);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// keep late patches (a stream that is still open) working
|
|
43
|
-
window.__unhead__ = { push: drain };
|
|
44
|
-
|
|
45
|
-
window.__yamfHead__ = head;
|
|
10
|
+
ensureClientHead();
|
|
46
11
|
}
|
|
47
12
|
|
|
48
13
|
/**
|
package/src/island/client.tsx
CHANGED
|
@@ -2,10 +2,16 @@ import { parse } from "devalue";
|
|
|
2
2
|
import type { FC } from "hono/jsx";
|
|
3
3
|
import { hydrateRoot } from "hono/jsx/dom/client";
|
|
4
4
|
import { withLeadingSlash } from "ufo";
|
|
5
|
+
import { ensureClientHead } from "./head";
|
|
5
6
|
import type { IslandClientDirectiveSerialized } from "./types";
|
|
6
7
|
|
|
7
8
|
declare let __yamf_raw_import__: <T>(file: string) => Promise<T>;
|
|
8
9
|
|
|
10
|
+
// this runtime loads whenever a page contains islands — set up the client
|
|
11
|
+
// head here so the handshake payload and streamed head patches actually
|
|
12
|
+
// apply, even when nothing imports the useHead hook
|
|
13
|
+
ensureClientHead();
|
|
14
|
+
|
|
9
15
|
const listeners: WeakMap<Element, VoidFunction> = new WeakMap();
|
|
10
16
|
|
|
11
17
|
const observer: IntersectionObserver = new IntersectionObserver(entries => {
|
|
@@ -45,7 +51,11 @@ const requestIdle = (callback: () => void): void => {
|
|
|
45
51
|
* element (src/client/index.ts) once this runtime module has been loaded.
|
|
46
52
|
*/
|
|
47
53
|
export const hydrateIsland = (island: HTMLElement): void => {
|
|
48
|
-
|
|
54
|
+
// skip islands omit the attribute entirely — parse after (and only if) it
|
|
55
|
+
// exists, otherwise parse("{}") would throw "Invalid input" and abort the
|
|
56
|
+
// whole pending-hydration loop
|
|
57
|
+
const rawProps = island.getAttribute("island-props");
|
|
58
|
+
const islandProps = rawProps === null ? [] : parse(rawProps);
|
|
49
59
|
const islandSrc = island.getAttribute("island-src");
|
|
50
60
|
const islandEntry = island.getAttribute("island-entry");
|
|
51
61
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHead } from "unhead/client";
|
|
2
|
+
import type { ClientUnhead } from "unhead/client";
|
|
3
|
+
import type { ResolvableHead } from "unhead/types";
|
|
4
|
+
|
|
5
|
+
interface StreamQueue {
|
|
6
|
+
_q?: unknown[][];
|
|
7
|
+
push?: (batch: unknown[]) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
__unhead__?: StreamQueue;
|
|
13
|
+
__yamfHead__?: ClientUnhead;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the client head, drains everything the server streamed into
|
|
19
|
+
* `window.__unhead__` (the handshake payload and streamed suspense patches)
|
|
20
|
+
* and upgrades the stub so late patches from an open stream keep applying.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent: it is called from the island hydration runtime (which loads
|
|
23
|
+
* whenever a page contains islands) and from the `useHead` hook module —
|
|
24
|
+
* whichever runs first wins.
|
|
25
|
+
*/
|
|
26
|
+
export const ensureClientHead = (): ClientUnhead => {
|
|
27
|
+
if (window.__yamfHead__) {
|
|
28
|
+
return window.__yamfHead__;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const head = createHead();
|
|
32
|
+
|
|
33
|
+
const drain = (batch: unknown[]): void => {
|
|
34
|
+
for (const input of batch) {
|
|
35
|
+
head.push(input as ResolvableHead);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// inline scripts (the handshake payload and streamed suspense patches)
|
|
40
|
+
// queue into window.__unhead__ while the document parses; drain them into
|
|
41
|
+
// the client head
|
|
42
|
+
const queue = window.__unhead__?._q;
|
|
43
|
+
|
|
44
|
+
if (queue) {
|
|
45
|
+
for (const batch of queue) {
|
|
46
|
+
drain(batch);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// keep late patches (a stream that is still open) working
|
|
51
|
+
window.__unhead__ = { push: drain };
|
|
52
|
+
|
|
53
|
+
window.__yamfHead__ = head;
|
|
54
|
+
|
|
55
|
+
return head;
|
|
56
|
+
};
|
package/src/server/entry.tsx
CHANGED
|
@@ -42,8 +42,8 @@ for (const [relativePath, handler] of Object.entries(pages).toSorted((a, b) =>
|
|
|
42
42
|
.replace(/\.[A-Za-z]+$/, "")
|
|
43
43
|
.replace(/\(([^(/\\]+)\)[/\\]/g, "")
|
|
44
44
|
.replace(/\[\.{3}]/g, "**")
|
|
45
|
-
.replace(/\[\.{3}([^\]]+)]/g, (_, p: string) => "**:" + p.replace(/[^\w
|
|
46
|
-
.replace(/\[([^/\]]+)]/g, (_, p: string) => ":" + p.replace(/[^\w
|
|
45
|
+
.replace(/\[\.{3}([^\]]+)]/g, (_, p: string) => "**:" + p.replace(/[^\w]/g, "_"))
|
|
46
|
+
.replace(/\[([^/\]]+)]/g, (_, p: string) => ":" + p.replace(/[^\w]/g, "_"))
|
|
47
47
|
.replace(/(\/|^)index$/, "") || "/";
|
|
48
48
|
|
|
49
49
|
route = withLeadingSlash(withoutTrailingSlash(route));
|