@pajecawav/yamf 0.0.8 → 0.0.10
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 +63 -7
- package/dist/client/index.js +39 -1
- package/dist/client/index.js.map +1 -0
- package/dist/components/Head.d.ts +2 -2
- package/dist/components/Head.js.map +1 -1
- package/dist/hooks/isPrerendering.d.ts +12 -0
- package/dist/hooks/isPrerendering.d.ts.map +1 -0
- package/dist/hooks/isPrerendering.js +17 -0
- package/dist/hooks/isPrerendering.js.map +1 -0
- package/dist/hooks/useHead.d.ts +9 -3
- package/dist/hooks/useHead.d.ts.map +1 -1
- package/dist/hooks/useHead.js +46 -4
- package/dist/hooks/useHead.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/island/client.js +45 -36
- package/dist/island/client.js.map +1 -1
- package/dist/page.d.ts +31 -4
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +60 -6
- package/dist/page.js.map +1 -1
- package/dist/safeAsync.d.ts +19 -0
- package/dist/safeAsync.d.ts.map +1 -0
- package/dist/safeAsync.js +34 -0
- package/dist/safeAsync.js.map +1 -0
- package/dist/server/entry.d.mts +9 -0
- package/dist/server/entry.d.mts.map +1 -1
- package/dist/server/entry.mjs +7 -2
- package/dist/server/entry.mjs.map +1 -1
- package/dist/server/island/server.d.mts.map +1 -1
- package/dist/server/island/server.mjs +11 -1
- package/dist/server/island/server.mjs.map +1 -1
- package/dist/vite/index.d.mts.map +1 -1
- package/dist/vite/index.mjs +12 -4
- package/dist/vite/index.mjs.map +1 -1
- package/dist/vite/islands.mjs +34 -5
- package/dist/vite/islands.mjs.map +1 -1
- package/dist/vite/prerender.mjs +16 -0
- package/dist/vite/prerender.mjs.map +1 -0
- package/dist/vite/virtual-root.mjs +11 -4
- package/dist/vite/virtual-root.mjs.map +1 -1
- package/dist/vite/virtual-template.mjs +8 -3
- package/dist/vite/virtual-template.mjs.map +1 -1
- package/package.json +23 -16
- package/src/client/index.ts +51 -1
- package/src/components/Head.tsx +2 -2
- package/src/hooks/isPrerendering.ts +13 -0
- package/src/hooks/useHead.tsx +98 -9
- package/src/index.ts +10 -1
- package/src/island/client.tsx +77 -65
- package/src/island/server.tsx +24 -1
- package/src/page.tsx +176 -11
- package/src/safeAsync.tsx +46 -0
- package/src/server/entry.tsx +25 -1
- package/src/vite/index.ts +30 -3
- package/src/vite/islands.ts +59 -5
- package/src/vite/prerender.ts +27 -0
- package/src/vite/virtual-root.ts +14 -4
- package/src/vite/virtual-template.ts +15 -3
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.10",
|
|
5
5
|
"description": "Yet another meta framework",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/pajecawav/yamf#readme",
|
|
@@ -30,36 +30,43 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"hono": "^4.
|
|
34
|
-
"vite": "^8.
|
|
33
|
+
"hono": "^4.13.9",
|
|
34
|
+
"vite": "^8.3.0",
|
|
35
|
+
"wouter": "^3.11.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"wouter": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
35
41
|
},
|
|
36
42
|
"dependencies": {
|
|
37
43
|
"@hono/react-compat": "^0.0.3",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
44
|
+
"@standard-schema/spec": "^1.1.0",
|
|
45
|
+
"devalue": "^6.0.2",
|
|
46
|
+
"exsolve": "^1.1.3",
|
|
40
47
|
"nitro": "3.0.260903-beta",
|
|
41
48
|
"rolldown-string": "^0.3.1",
|
|
42
|
-
"rou3": "^0.
|
|
49
|
+
"rou3": "^1.0.0",
|
|
43
50
|
"ufo": "^1.6.4",
|
|
44
|
-
"unhead": "^3.4.
|
|
45
|
-
"wouter": "^3.
|
|
51
|
+
"unhead": "^3.4.2",
|
|
52
|
+
"wouter": "^3.13.0"
|
|
46
53
|
},
|
|
47
54
|
"devDependencies": {
|
|
48
55
|
"@hono/react-compat": "^0.0.3",
|
|
49
|
-
"@pajecawav/tools": "^0.0
|
|
56
|
+
"@pajecawav/tools": "^0.1.0",
|
|
50
57
|
"@playwright/test": "^1.63.0",
|
|
51
|
-
"@types/node": "^26.4
|
|
58
|
+
"@types/node": "^26.6.4",
|
|
52
59
|
"cross-env": "^10.1.0",
|
|
53
|
-
"hono": "^4.13.
|
|
60
|
+
"hono": "^4.13.12",
|
|
54
61
|
"husky": "^9.1.7",
|
|
55
62
|
"npm-run-all2": "^9.0.3",
|
|
56
|
-
"oxfmt": "^0.
|
|
57
|
-
"oxlint": "^1.
|
|
58
|
-
"oxlint-tsgolint": "^7.0.
|
|
59
|
-
"publint": "^0.3.
|
|
63
|
+
"oxfmt": "^0.71.0",
|
|
64
|
+
"oxlint": "^1.86.0",
|
|
65
|
+
"oxlint-tsgolint": "^7.0.2002",
|
|
66
|
+
"publint": "^0.3.25",
|
|
60
67
|
"tsdown": "0.23.0",
|
|
61
68
|
"typescript": "~7.0.2",
|
|
62
|
-
"vite": "^8.
|
|
69
|
+
"vite": "^8.3.2"
|
|
63
70
|
},
|
|
64
71
|
"scripts": {
|
|
65
72
|
"build": "tsdown",
|
package/src/client/index.ts
CHANGED
|
@@ -1 +1,51 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Tiny bootstrap for the island runtime.
|
|
3
|
+
*
|
|
4
|
+
* Defines the <yamf-island> custom element and dynamically imports the real
|
|
5
|
+
* hydration runtime (hono/jsx dom renderer, devalue, unhead client) only when
|
|
6
|
+
* the document actually contains an island. Pages — or whole apps — without
|
|
7
|
+
* islands never pay for it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const pending: Set<HTMLElement> = new Set();
|
|
11
|
+
|
|
12
|
+
type Hydrate = (island: HTMLElement) => void;
|
|
13
|
+
|
|
14
|
+
let hydrate: Hydrate | null = null;
|
|
15
|
+
let loading: boolean = false;
|
|
16
|
+
|
|
17
|
+
const loadRuntime = (): void => {
|
|
18
|
+
if (loading) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
loading = true;
|
|
23
|
+
|
|
24
|
+
void import("../island/client").then(({ hydrateIsland }) => {
|
|
25
|
+
hydrate = hydrateIsland;
|
|
26
|
+
|
|
27
|
+
for (const island of pending) {
|
|
28
|
+
pending.delete(island);
|
|
29
|
+
hydrate(island);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
customElements.define(
|
|
35
|
+
"yamf-island",
|
|
36
|
+
class extends HTMLElement {
|
|
37
|
+
public connectedCallback() {
|
|
38
|
+
if (hydrate) {
|
|
39
|
+
hydrate(this);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pending.add(this);
|
|
44
|
+
loadRuntime();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public disconnectedCallback() {
|
|
48
|
+
pending.delete(this);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
);
|
package/src/components/Head.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ResolvableHead } from "unhead/types";
|
|
2
1
|
import { useHead } from "#/hooks/useHead";
|
|
2
|
+
import type { YamfHead } from "#/shared/head";
|
|
3
3
|
|
|
4
|
-
export type HeadProps =
|
|
4
|
+
export type HeadProps = YamfHead;
|
|
5
5
|
|
|
6
6
|
export const Head = (props: HeadProps): void => {
|
|
7
7
|
useHead(props);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSSRContext } from "#/context/ssr";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether the current render runs inside Nitro's build-time prerender pass —
|
|
5
|
+
* the prerenderer fetching the route to write it to disk as a static file.
|
|
6
|
+
*
|
|
7
|
+
* Always `false` in dev and at request time. Prefer this over
|
|
8
|
+
* `import.meta.prerender`: pages are bundled by the vite ssr service, where
|
|
9
|
+
* that flag is never replaced and would always read as `undefined`.
|
|
10
|
+
*/
|
|
11
|
+
export const isPrerendering = (): boolean => {
|
|
12
|
+
return useSSRContext()?.event.req.headers.has("x-nitro-prerender") ?? false;
|
|
13
|
+
};
|
package/src/hooks/useHead.tsx
CHANGED
|
@@ -1,39 +1,128 @@
|
|
|
1
|
+
import { useEffect, useRef } from "hono/jsx";
|
|
1
2
|
import { useHead as _useHead, useSeoMeta as _useSeoMeta } from "unhead";
|
|
2
3
|
import type { ClientUnhead } from "unhead/client";
|
|
3
4
|
import { createHead } from "unhead/client";
|
|
4
|
-
import type { ResolvableHead, UseSeoMetaInput } from "unhead/types";
|
|
5
|
+
import type { ActiveHeadEntry, ResolvableHead, UseSeoMetaInput } from "unhead/types";
|
|
5
6
|
import { useSSRContext } from "#/context/ssr";
|
|
7
|
+
import type { YamfHead } from "#/shared/head";
|
|
8
|
+
|
|
9
|
+
interface StreamQueue {
|
|
10
|
+
_q?: unknown[][];
|
|
11
|
+
push?: (batch: unknown[]) => void;
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
declare global {
|
|
8
15
|
interface Window {
|
|
9
|
-
|
|
16
|
+
__unhead__?: StreamQueue;
|
|
17
|
+
__yamfHead__?: ClientUnhead;
|
|
10
18
|
}
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
if (!import.meta.env.SSR) {
|
|
14
|
-
|
|
22
|
+
const head = createHead();
|
|
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;
|
|
15
46
|
}
|
|
16
47
|
|
|
17
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Binds an unhead entry to the calling component's lifecycle: the entry is
|
|
50
|
+
* created once on mount, patched on every re-render with the latest input,
|
|
51
|
+
* and disposed on unmount. Without this, every re-render pushes a new entry
|
|
52
|
+
* that is never removed — a leak and growing DOM render cost.
|
|
53
|
+
*/
|
|
54
|
+
const useHeadEntryLifecycle = <T,>(
|
|
55
|
+
input: T | undefined,
|
|
56
|
+
createEntry: (head: ClientUnhead, input: T) => ActiveHeadEntry<T>,
|
|
57
|
+
): void => {
|
|
58
|
+
const entryRef = useRef<ActiveHeadEntry<T> | null>(null);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const head = window.__yamfHead__;
|
|
62
|
+
|
|
63
|
+
if (!head || input === undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entry = createEntry(head, input);
|
|
68
|
+
|
|
69
|
+
entryRef.current = entry;
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
entry.dispose();
|
|
73
|
+
entryRef.current = null;
|
|
74
|
+
};
|
|
75
|
+
// mount-only: the entry is created once per component instance
|
|
76
|
+
// oxlint-disable-next-line eslint(exhaustive-deps)
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (input !== undefined) {
|
|
81
|
+
entryRef.current?.patch(input);
|
|
82
|
+
}
|
|
83
|
+
}, [input]);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const useHead = (input?: YamfHead): void => {
|
|
18
87
|
if (import.meta.env.SSR) {
|
|
88
|
+
if (input === undefined) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
19
92
|
const ctx = useSSRContext();
|
|
20
93
|
|
|
21
94
|
if (ctx?.head) {
|
|
22
|
-
|
|
95
|
+
// the custom `seo` key is yamf sugar for useSeoMeta — process it
|
|
96
|
+
// instead of passing an unknown key down to unhead
|
|
97
|
+
const { seo, ...rest } = input;
|
|
98
|
+
|
|
99
|
+
if (seo !== undefined) {
|
|
100
|
+
_useSeoMeta(ctx.head, seo);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_useHead(ctx.head, rest);
|
|
23
104
|
}
|
|
24
|
-
|
|
25
|
-
|
|
105
|
+
|
|
106
|
+
return;
|
|
26
107
|
}
|
|
108
|
+
|
|
109
|
+
useHeadEntryLifecycle(input, (head, resolved) => _useHead(head, resolved));
|
|
27
110
|
};
|
|
28
111
|
|
|
29
112
|
export const useSeoMeta = (input?: UseSeoMetaInput): void => {
|
|
30
113
|
if (import.meta.env.SSR) {
|
|
114
|
+
if (input === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
31
118
|
const ctx = useSSRContext();
|
|
32
119
|
|
|
33
120
|
if (ctx?.head) {
|
|
34
121
|
_useSeoMeta(ctx.head, input);
|
|
35
122
|
}
|
|
36
|
-
|
|
37
|
-
|
|
123
|
+
|
|
124
|
+
return;
|
|
38
125
|
}
|
|
126
|
+
|
|
127
|
+
useHeadEntryLifecycle(input, (head, resolved) => _useSeoMeta(head, resolved));
|
|
39
128
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
export { definePage } from "./page";
|
|
2
|
-
export type { PageHandler } from "./page";
|
|
2
|
+
export type { PageCacheOptions, PageHandler, PageRenderer } from "./page";
|
|
3
|
+
|
|
4
|
+
export type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
5
|
+
|
|
6
|
+
// re-exported so apps can annotate render args without depending on unhead
|
|
7
|
+
export type { Unhead } from "unhead/server";
|
|
8
|
+
|
|
9
|
+
export { safeAsync } from "./safeAsync";
|
|
10
|
+
export type { SafeAsyncFallbackProps } from "./safeAsync";
|
|
3
11
|
|
|
4
12
|
export { useSSRContext } from "./context/ssr";
|
|
5
13
|
export { useEvent } from "./hooks/useEvent";
|
|
14
|
+
export { isPrerendering } from "./hooks/isPrerendering";
|
|
6
15
|
export { useHead, useSeoMeta } from "./hooks/useHead";
|
|
7
16
|
|
|
8
17
|
export type { IslandClientDirective, IslandProps } from "./island/types";
|
package/src/island/client.tsx
CHANGED
|
@@ -4,11 +4,11 @@ import { hydrateRoot } from "hono/jsx/dom/client";
|
|
|
4
4
|
import { withLeadingSlash } from "ufo";
|
|
5
5
|
import type { IslandClientDirectiveSerialized } from "./types";
|
|
6
6
|
|
|
7
|
-
declare let
|
|
7
|
+
declare let __yamf_raw_import__: <T>(file: string) => Promise<T>;
|
|
8
8
|
|
|
9
|
-
const listeners
|
|
9
|
+
const listeners: WeakMap<Element, VoidFunction> = new WeakMap();
|
|
10
10
|
|
|
11
|
-
const observer = new IntersectionObserver(entries => {
|
|
11
|
+
const observer: IntersectionObserver = new IntersectionObserver(entries => {
|
|
12
12
|
for (const entry of entries) {
|
|
13
13
|
if (entry.isIntersecting) {
|
|
14
14
|
listeners.get(entry.target)?.();
|
|
@@ -27,69 +27,81 @@ const unobserve = (target: Element) => {
|
|
|
27
27
|
listeners.delete(target);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const islandProps = parse(this.getAttribute("island-props") ?? "{}");
|
|
35
|
-
const islandSrc = this.getAttribute("island-src");
|
|
36
|
-
const islandEntry = this.getAttribute("island-entry");
|
|
37
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
38
|
-
const islandClient = (this.getAttribute("island-client") ??
|
|
39
|
-
"load") as IslandClientDirectiveSerialized;
|
|
40
|
-
|
|
41
|
-
if (!islandSrc) {
|
|
42
|
-
throw new Error("Missing island-src attribute");
|
|
43
|
-
}
|
|
30
|
+
// Safari does not enable requestIdleCallback by default in any stable
|
|
31
|
+
// release — without a fallback, client:idle islands never hydrate there
|
|
32
|
+
const requestIdle = (callback: () => void): void => {
|
|
33
|
+
const ric = window.requestIdleCallback;
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
if (typeof ric === "function") {
|
|
36
|
+
ric(callback);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
break;
|
|
72
|
-
case "visible":
|
|
73
|
-
// yamf-island has `display: contents` which breaks IntersectionObserver
|
|
74
|
-
// so we have to observe the first child instead if it exists
|
|
75
|
-
if (this.firstElementChild) {
|
|
76
|
-
observe(this.firstElementChild, initIsland);
|
|
77
|
-
} else {
|
|
78
|
-
initIsland();
|
|
79
|
-
}
|
|
80
|
-
break;
|
|
81
|
-
case "false":
|
|
82
|
-
case "skip":
|
|
83
|
-
break;
|
|
84
|
-
default:
|
|
85
|
-
islandClient satisfies never;
|
|
86
|
-
// oxlint-disable-next-line typescript/restrict-template-expressions
|
|
87
|
-
throw new Error(`Invalid island-client value: ${islandClient}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
40
|
+
setTimeout(callback, 1);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hydrates a single <yamf-island> island. Called by the bootstrap custom
|
|
45
|
+
* element (src/client/index.ts) once this runtime module has been loaded.
|
|
46
|
+
*/
|
|
47
|
+
export const hydrateIsland = (island: HTMLElement): void => {
|
|
48
|
+
// skip islands omit the attribute entirely — parse after (and only if) it
|
|
49
|
+
// exists, otherwise parse("{}") would throw "Invalid input" and abort the
|
|
50
|
+
// whole pending-hydration loop
|
|
51
|
+
const rawProps = island.getAttribute("island-props");
|
|
52
|
+
const islandProps = rawProps === null ? [] : parse(rawProps);
|
|
53
|
+
const islandSrc = island.getAttribute("island-src");
|
|
54
|
+
const islandEntry = island.getAttribute("island-entry");
|
|
55
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
56
|
+
const islandClient = (island.getAttribute("island-client") ??
|
|
57
|
+
"load") as IslandClientDirectiveSerialized;
|
|
58
|
+
|
|
59
|
+
if (!islandSrc) {
|
|
60
|
+
throw new Error("Missing island-src attribute");
|
|
61
|
+
}
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
63
|
+
if (!islandEntry) {
|
|
64
|
+
throw new Error("Missing island-entry attribute");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hydrate = (mod: Record<string, FC>) => {
|
|
68
|
+
const Comp = mod[islandEntry];
|
|
69
|
+
|
|
70
|
+
if (!Comp) {
|
|
71
|
+
throw new Error(`Missing island entry ${islandEntry} in ${islandSrc}`);
|
|
93
72
|
}
|
|
94
|
-
|
|
95
|
-
);
|
|
73
|
+
|
|
74
|
+
hydrateRoot(island, <Comp {...islandProps} />);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const initIsland = () => {
|
|
78
|
+
const src = withLeadingSlash(islandSrc);
|
|
79
|
+
void __yamf_raw_import__<Record<string, FC>>(src).then(hydrate);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
switch (islandClient) {
|
|
83
|
+
case "true":
|
|
84
|
+
case "load":
|
|
85
|
+
initIsland();
|
|
86
|
+
break;
|
|
87
|
+
case "idle":
|
|
88
|
+
requestIdle(initIsland);
|
|
89
|
+
break;
|
|
90
|
+
case "visible":
|
|
91
|
+
// yamf-island has `display: contents` which breaks IntersectionObserver
|
|
92
|
+
// so we have to observe the first child instead if it exists
|
|
93
|
+
if (island.firstElementChild) {
|
|
94
|
+
observe(island.firstElementChild, initIsland);
|
|
95
|
+
} else {
|
|
96
|
+
initIsland();
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "false":
|
|
100
|
+
case "skip":
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
islandClient satisfies never;
|
|
104
|
+
// oxlint-disable-next-line typescript/restrict-template-expressions
|
|
105
|
+
throw new Error(`Invalid island-client value: ${islandClient}`);
|
|
106
|
+
}
|
|
107
|
+
};
|
package/src/island/server.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import type { Child, FC } from "hono/jsx";
|
|
|
3
3
|
import type { ImportAssetsResultRaw } from "#/shared/assets";
|
|
4
4
|
import type { IslandClientDirective, IslandProps } from "./types";
|
|
5
5
|
|
|
6
|
+
// configurable via the YAMF_ISLAND_PROPS_LIMIT env / define
|
|
7
|
+
const PROPS_WARNING_LIMIT = Number(import.meta.env["YAMF_ISLAND_PROPS_LIMIT"]) || 16 * 1024;
|
|
8
|
+
|
|
6
9
|
declare module "hono/jsx" {
|
|
7
10
|
namespace JSX {
|
|
8
11
|
interface IntrinsicElements {
|
|
@@ -18,6 +21,10 @@ declare module "hono/jsx" {
|
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
const isSkipDirective = (directive: IslandClientDirective | undefined): boolean => {
|
|
25
|
+
return directive === false || directive === "skip";
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
export const createIsland = (
|
|
22
29
|
Component: FC,
|
|
23
30
|
exportName: string,
|
|
@@ -31,9 +38,25 @@ export const createIsland = (
|
|
|
31
38
|
throw new Error(`Missing island entry for island ${Component.name}`);
|
|
32
39
|
}
|
|
33
40
|
|
|
41
|
+
// skip islands are never hydrated — serializing their props into the
|
|
42
|
+
// attribute would be pure dead weight in the HTML
|
|
43
|
+
const skip = isSkipDirective(clientDirective);
|
|
44
|
+
|
|
45
|
+
let islandProps: string | undefined;
|
|
46
|
+
|
|
47
|
+
if (!skip) {
|
|
48
|
+
islandProps = stringify(props);
|
|
49
|
+
|
|
50
|
+
if (import.meta.env.DEV && islandProps.length > PROPS_WARNING_LIMIT) {
|
|
51
|
+
console.warn(
|
|
52
|
+
`[yamf] island "${exportName}" receives ${(islandProps.length / 1024).toFixed(1)} KB of serialized props — consider slimming them down (limit: ${PROPS_WARNING_LIMIT} bytes, configurable via YAMF_ISLAND_PROPS_LIMIT)`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
return (
|
|
35
58
|
<yamf-island
|
|
36
|
-
island-props={
|
|
59
|
+
island-props={islandProps}
|
|
37
60
|
island-src={assets.entry}
|
|
38
61
|
island-entry={exportName}
|
|
39
62
|
island-client={clientDirective}
|