hono-decks 1.2.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/adapters/hono-markdown.js +33 -1
- package/dist/advanced.js +233 -14
- package/dist/bin.js +42 -6
- package/dist/cli.js +42 -6
- package/dist/client.js +14 -2
- package/dist/node.js +274 -19
- package/dist/vite.js +61 -8
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -294,7 +294,7 @@ export default defineConfig({
|
|
|
294
294
|
});
|
|
295
295
|
```
|
|
296
296
|
|
|
297
|
-
pluginはVite起動前にcompileし、deck rootと`hono-decks.config.ts`をVite watcherへ登録します。再compile
|
|
297
|
+
pluginはVite起動前にcompileし、deck rootと`hono-decks.config.ts`をVite watcherへ登録します。再compileに成功すると、既存のislandを安全に識別できる場合は表示ページを再読み込みせずに内容を更新します。対応するprops・Markdownコンテキストの変更、別々のislandの並べ替え・追加・削除でも状態を維持し、現在のスライド・ステップを保ちます。クライアントコードの変更や同一islandの区別がつかない場合など、安全に更新できないときは表示ページを再読み込みしてスライドとステップを復元します。このときislandの状態はリセットされます。どちらの構成でも通常の`bun run dev`だけでauthoringできます。
|
|
298
298
|
|
|
299
299
|
## Runtime configuration
|
|
300
300
|
|
package/README.md
CHANGED
|
@@ -295,7 +295,7 @@ export default defineConfig({
|
|
|
295
295
|
});
|
|
296
296
|
~~~
|
|
297
297
|
|
|
298
|
-
The plugin compiles before Vite starts and adds the deck root and <code>hono-decks.config.ts</code> to the Vite watcher. After a successful recompile,
|
|
298
|
+
The plugin compiles before Vite starts and adds the deck root and <code>hono-decks.config.ts</code> to the Vite watcher. After a successful recompile, presentation pages update without reloading when existing islands can be identified safely. Their state survives supported prop and Markdown context changes, reordering, and the addition or removal of distinct islands. The current slide and step are retained. When an update cannot be applied safely, such as a changed client bundle or ambiguous duplicate islands, the presentation page reloads and restores the slide and step; island state is then reset. In both setups, the application's normal <code>dev</code> command is enough for authoring.
|
|
299
299
|
|
|
300
300
|
## Runtime configuration
|
|
301
301
|
|
|
@@ -31,8 +31,37 @@ function createMarkdownAdapter({ cwd, options }) {
|
|
|
31
31
|
function emitMarkdownClient(islands, runtime) {
|
|
32
32
|
if (!islands.length) return "";
|
|
33
33
|
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
34
|
+
const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
|
|
35
|
+
const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
|
|
36
|
+
const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
|
|
37
|
+
const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
|
|
34
38
|
return `import { hydrateMarkdownIslands } from ${literal(runtime)};
|
|
39
|
+
import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
|
|
40
|
+
import { MarkdownContext } from ${literal(contextModule)};
|
|
41
|
+
import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
|
|
42
|
+
import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
|
|
43
|
+
function hydrateDeckMarkdownIslands() {
|
|
35
44
|
hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
|
|
45
|
+
schedule(element, loader, props, strategy, mount) {
|
|
46
|
+
return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
|
|
47
|
+
const live = window.__honoDecksLive;
|
|
48
|
+
if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
|
|
49
|
+
const Component = module.default;
|
|
50
|
+
function LiveMarkdownIsland(componentProps) {
|
|
51
|
+
const [current, setCurrent] = useLiveState({
|
|
52
|
+
props: componentProps,
|
|
53
|
+
context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
|
|
54
|
+
});
|
|
55
|
+
const [active, setActive] = useLiveState(true);
|
|
56
|
+
live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
|
|
57
|
+
return active ? liveJsx(MarkdownContext.Provider, {
|
|
58
|
+
value: current.context,
|
|
59
|
+
children: liveJsx(Component, current.props),
|
|
60
|
+
}) : null;
|
|
61
|
+
}
|
|
62
|
+
return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
|
|
63
|
+
});
|
|
64
|
+
},
|
|
36
65
|
ready(element, start) {
|
|
37
66
|
const slide = element.closest('section[data-slide-state]');
|
|
38
67
|
if (!slide || slide.dataset.slideState === 'active') { start(); return; }
|
|
@@ -41,7 +70,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
|
|
|
41
70
|
});
|
|
42
71
|
observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
|
|
43
72
|
}
|
|
44
|
-
})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
hydrateDeckMarkdownIslands();
|
|
76
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
|
|
45
77
|
}
|
|
46
78
|
//#endregion
|
|
47
79
|
export { createHonoMarkdownAdapter, createMarkdownAdapter };
|
package/dist/advanced.js
CHANGED
|
@@ -804,19 +804,209 @@ function escapeHtml$7(value) {
|
|
|
804
804
|
function renderLiveReloadScript(eventsPath, nonce) {
|
|
805
805
|
return `<script${documentNonceAttribute(nonce)}>
|
|
806
806
|
(() => {
|
|
807
|
-
const
|
|
808
|
-
const
|
|
809
|
-
|
|
807
|
+
const events = new EventSource(${JSON.stringify(eventsPath)});
|
|
808
|
+
const scriptNonce = document.currentScript?.nonce;
|
|
809
|
+
let running = false;
|
|
810
|
+
let pending = false;
|
|
811
|
+
const selector = "[data-hono-decks-island],[data-hono-md-island]";
|
|
812
|
+
const islandUpdaters = new WeakMap();
|
|
813
|
+
const hydrators = new Set();
|
|
814
|
+
window.__honoDecksLive = {
|
|
815
|
+
register: (element, update, dispose) => islandUpdaters.set(element, { update, dispose }),
|
|
816
|
+
registerHydrator: (hydrate) => hydrators.add(hydrate),
|
|
817
|
+
};
|
|
818
|
+
const clientSelector = "script[data-hono-decks-client-entry]";
|
|
819
|
+
const clientText = async (url) => {
|
|
820
|
+
if (!url) return "";
|
|
821
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
822
|
+
if (!response.ok) throw new Error("Client entry fetch failed");
|
|
823
|
+
return response.text();
|
|
824
|
+
};
|
|
825
|
+
const domReady = document.readyState === "loading"
|
|
826
|
+
? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
|
|
827
|
+
: Promise.resolve();
|
|
828
|
+
let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
|
|
829
|
+
|
|
830
|
+
function signature(element, includeContext = true, includeProps = true) {
|
|
831
|
+
const attributes = element.hasAttribute("data-hono-md-island")
|
|
832
|
+
? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
|
|
833
|
+
: ["data-hono-decks-island", "data-hono-decks-props"];
|
|
834
|
+
return JSON.stringify([attributes.map((name) => !includeProps && name.endsWith("-props") ? null : element.getAttribute(name)),
|
|
835
|
+
includeContext ? element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null : null]);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function update() {
|
|
839
|
+
const response = await fetch(location.href, { cache: "no-store" });
|
|
840
|
+
if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
|
|
841
|
+
const next = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
842
|
+
const currentDeck = document.querySelector("[data-hono-decks-deck]");
|
|
843
|
+
const nextDeck = next.querySelector("[data-hono-decks-deck]");
|
|
844
|
+
const style = document.querySelector("[data-hono-decks-presentation-style]");
|
|
845
|
+
const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
|
|
846
|
+
const runtime = window.__honoDecksPresentationRuntime;
|
|
847
|
+
if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
|
|
848
|
+
const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
|
|
849
|
+
const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
|
|
850
|
+
if (!newSlides.length) throw new Error("Empty deck");
|
|
851
|
+
// Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
|
|
852
|
+
if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
|
|
853
|
+
const collect = (slides) => slides.flatMap((slide, slideIndex) =>
|
|
854
|
+
Array.from(slide.querySelectorAll(selector)).map((element, position) => ({ element, slideIndex, position })));
|
|
855
|
+
const oldItems = collect(oldSlides);
|
|
856
|
+
const newItems = collect(newSlides);
|
|
857
|
+
const nextScript = next.querySelector(clientSelector);
|
|
858
|
+
const fetchedClient = await clientText(nextScript?.getAttribute("src"));
|
|
859
|
+
const previousClient = await clientSnapshot;
|
|
860
|
+
if (previousClient === null) throw new Error("Initial client entry fetch failed");
|
|
861
|
+
// Keep the loaded registry when the last island disappears; it can dispose
|
|
862
|
+
// that island and initialize it again if it is reintroduced later.
|
|
863
|
+
const nextClient = !nextScript && !newItems.length ? previousClient : fetchedClient;
|
|
864
|
+
// A changed client bundle could contain new implementations for running islands.
|
|
865
|
+
if (oldItems.length && previousClient !== nextClient) throw new Error("Client code changed");
|
|
866
|
+
const needsEntry = !document.querySelector(clientSelector) && Boolean(nextScript);
|
|
867
|
+
if (newItems.length && !nextScript) throw new Error("New islands require a client entry");
|
|
868
|
+
if (!needsEntry && previousClient !== nextClient) throw new Error("Client code changed");
|
|
869
|
+
|
|
870
|
+
const duplicateKeys = new Set([...oldItems, ...newItems].map((item) => signature(item.element, false)));
|
|
871
|
+
for (const key of duplicateKeys) {
|
|
872
|
+
const before = oldItems.filter((item) => signature(item.element, false) === key);
|
|
873
|
+
const after = newItems.filter((item) => signature(item.element, false) === key);
|
|
874
|
+
if (before.length <= 1 && after.length <= 1) continue;
|
|
875
|
+
if (before.length !== after.length || before.some((item, index) =>
|
|
876
|
+
item.slideIndex !== after[index].slideIndex || item.position !== after[index].position))
|
|
877
|
+
throw new Error("Ambiguous duplicate islands");
|
|
878
|
+
}
|
|
879
|
+
const pairs = [];
|
|
880
|
+
const remainingOld = new Set(oldItems);
|
|
881
|
+
const remainingNew = new Set(newItems);
|
|
882
|
+
function pair(oldItem, newItem) {
|
|
883
|
+
remainingOld.delete(oldItem);
|
|
884
|
+
remainingNew.delete(newItem);
|
|
885
|
+
pairs.push([oldItem, newItem]);
|
|
886
|
+
}
|
|
887
|
+
// Exact matches can move between slides. Match only unique instances so
|
|
888
|
+
// duplicate islands cannot accidentally inherit each other's state.
|
|
889
|
+
for (const oldItem of oldItems) {
|
|
890
|
+
const key = signature(oldItem.element, false);
|
|
891
|
+
const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false) === key);
|
|
892
|
+
const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false) === key);
|
|
893
|
+
if (oldMatches.length === 1 && newMatches.length === 1) pair(oldItem, newMatches[0]);
|
|
894
|
+
}
|
|
895
|
+
// Changed props can be reconciled if there is only one island of that type.
|
|
896
|
+
for (const oldItem of Array.from(remainingOld)) {
|
|
897
|
+
const key = signature(oldItem.element, false, false);
|
|
898
|
+
const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false, false) === key);
|
|
899
|
+
const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false, false) === key);
|
|
900
|
+
if (oldMatches.length === 1 && newMatches.length === 1 &&
|
|
901
|
+
oldItem.slideIndex === newMatches[0].slideIndex && oldItem.position === newMatches[0].position)
|
|
902
|
+
pair(oldItem, newMatches[0]);
|
|
903
|
+
}
|
|
904
|
+
// Existing duplicate instances remain safe only when their positions and
|
|
905
|
+
// identity attributes are unchanged; ambiguous structural edits reload.
|
|
906
|
+
for (const oldItem of Array.from(remainingOld)) {
|
|
907
|
+
const newItem = Array.from(remainingNew).find((item) => item.slideIndex === oldItem.slideIndex && item.position === oldItem.position);
|
|
908
|
+
if (newItem && signature(oldItem.element, false) === signature(newItem.element, false)) pair(oldItem, newItem);
|
|
909
|
+
}
|
|
910
|
+
for (const oldItem of remainingOld) {
|
|
911
|
+
const key = signature(oldItem.element, false, false);
|
|
912
|
+
if (Array.from(remainingNew).some((item) => signature(item.element, false, false) === key))
|
|
913
|
+
throw new Error("Ambiguous island identity");
|
|
914
|
+
}
|
|
915
|
+
const removals = [];
|
|
916
|
+
for (const oldItem of remainingOld) {
|
|
917
|
+
const entry = islandUpdaters.get(oldItem.element);
|
|
918
|
+
if (!entry?.dispose) throw new Error("Cannot safely dispose island");
|
|
919
|
+
removals.push(entry.dispose);
|
|
920
|
+
}
|
|
921
|
+
const updates = [];
|
|
922
|
+
let preserveSlides = false;
|
|
923
|
+
for (const [oldItem, newItem] of pairs) {
|
|
924
|
+
const before = oldItem.element;
|
|
925
|
+
const after = newItem.element;
|
|
926
|
+
const pendingMarkdown = before.hasAttribute("data-hono-md-island") && !before.hasAttribute("data-hono-md-mounted");
|
|
927
|
+
if (pendingMarkdown) {
|
|
928
|
+
if (oldSlides.length !== newSlides.length || oldItem.slideIndex !== newItem.slideIndex)
|
|
929
|
+
throw new Error("Pending island moved between slides");
|
|
930
|
+
preserveSlides = true;
|
|
931
|
+
}
|
|
932
|
+
if (signature(before, !pendingMarkdown) !== signature(after, !pendingMarkdown)) {
|
|
933
|
+
const entry = islandUpdaters.get(before);
|
|
934
|
+
if (!entry?.update || signature(before, false, false) !== signature(after, false, false))
|
|
935
|
+
throw new Error("Island properties changed");
|
|
936
|
+
const markdown = before.hasAttribute("data-hono-md-island");
|
|
937
|
+
const propsAttribute = markdown ? "data-hono-md-props" : "data-hono-decks-props";
|
|
938
|
+
const rawProps = after.getAttribute(propsAttribute) || "{}";
|
|
939
|
+
const props = JSON.parse(rawProps);
|
|
940
|
+
const context = markdown ? JSON.parse(after.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") || "{}") : undefined;
|
|
941
|
+
updates.push([before, entry.update, propsAttribute, rawProps, markdown ? { props, context } : props]);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
for (const dispose of removals) dispose();
|
|
945
|
+
for (const [oldItem, newItem] of pairs) newItem.element.replaceWith(oldItem.element);
|
|
946
|
+
if (preserveSlides) {
|
|
947
|
+
for (let i = 0; i < oldSlides.length; i++) {
|
|
948
|
+
const oldSlide = oldSlides[i];
|
|
949
|
+
const newSlide = newSlides[i];
|
|
950
|
+
for (const attribute of Array.from(oldSlide.attributes)) oldSlide.removeAttribute(attribute.name);
|
|
951
|
+
for (const attribute of Array.from(newSlide.attributes)) oldSlide.setAttribute(attribute.name, attribute.value);
|
|
952
|
+
oldSlide.replaceChildren(...newSlide.childNodes);
|
|
953
|
+
newSlide.replaceWith(oldSlide);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
currentDeck.replaceChildren(...nextDeck.childNodes);
|
|
957
|
+
for (const [island, update, propsAttribute, rawProps, value] of updates) {
|
|
958
|
+
island.setAttribute(propsAttribute, rawProps);
|
|
959
|
+
update(value);
|
|
960
|
+
}
|
|
961
|
+
style.textContent = nextStyle.textContent;
|
|
962
|
+
document.title = next.title;
|
|
963
|
+
clientSnapshot = Promise.resolve(nextClient);
|
|
964
|
+
runtime.refresh();
|
|
965
|
+
if (remainingNew.size) {
|
|
966
|
+
if (needsEntry) {
|
|
967
|
+
const url = new URL(nextScript.getAttribute("src"), location.href);
|
|
968
|
+
if (url.origin !== location.origin) throw new Error("Cross-origin client entry");
|
|
969
|
+
url.searchParams.set("hono-decks-update", String(Date.now()));
|
|
970
|
+
const script = document.createElement("script");
|
|
971
|
+
script.type = "module";
|
|
972
|
+
script.setAttribute("data-hono-decks-client-entry", "");
|
|
973
|
+
if (scriptNonce) script.nonce = scriptNonce;
|
|
974
|
+
script.src = url.href;
|
|
975
|
+
await new Promise((resolve, reject) => {
|
|
976
|
+
script.onload = resolve;
|
|
977
|
+
script.onerror = () => reject(new Error("Client entry failed"));
|
|
978
|
+
document.body.appendChild(script);
|
|
979
|
+
});
|
|
980
|
+
} else {
|
|
981
|
+
for (const hydrate of hydrators) hydrate();
|
|
982
|
+
}
|
|
983
|
+
for (const item of remainingNew) {
|
|
984
|
+
const element = item.element;
|
|
985
|
+
if (!element.hasAttribute("data-hono-decks-live-mounted") && !element.hasAttribute("data-hono-md-scheduled"))
|
|
986
|
+
throw new Error("New island did not initialize");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function onUpdate() {
|
|
992
|
+
if (running) { pending = true; return; }
|
|
993
|
+
running = true;
|
|
994
|
+
try {
|
|
995
|
+
do { pending = false; await update(); } while (pending);
|
|
996
|
+
} catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
|
|
997
|
+
finally { running = false; }
|
|
998
|
+
}
|
|
999
|
+
events.addEventListener("deck:updated", onUpdate);
|
|
810
1000
|
})();
|
|
811
1001
|
<\/script>`;
|
|
812
1002
|
}
|
|
813
1003
|
function renderClientEntryScript(clientEntry, nonce) {
|
|
814
|
-
return `<script type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
|
|
1004
|
+
return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
|
|
815
1005
|
}
|
|
816
1006
|
function renderPresentationScript(nonce) {
|
|
817
1007
|
return `<script${documentNonceAttribute(nonce)}>
|
|
818
1008
|
(() => {
|
|
819
|
-
|
|
1009
|
+
let slides = Array.from(document.querySelectorAll(".slide"));
|
|
820
1010
|
const stage = document.querySelector("[data-hono-decks-stage]");
|
|
821
1011
|
const deck = document.querySelector("[data-hono-decks-deck]");
|
|
822
1012
|
const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
|
|
@@ -1090,6 +1280,7 @@ function renderPresentationScript(nonce) {
|
|
|
1090
1280
|
updateFires(nextStepIndex);
|
|
1091
1281
|
publishState();
|
|
1092
1282
|
requestAnimationFrame(() => {
|
|
1283
|
+
if (token !== transitionToken) return;
|
|
1093
1284
|
setSlideState(outgoing, "leaving", direction, transition, timing);
|
|
1094
1285
|
setSlideState(incoming, "active", direction, transition, timing);
|
|
1095
1286
|
waitForSlideTransition(outgoing, incoming, direction, token);
|
|
@@ -1196,13 +1387,41 @@ function renderPresentationScript(nonce) {
|
|
|
1196
1387
|
await document.documentElement.requestFullscreen?.();
|
|
1197
1388
|
}
|
|
1198
1389
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1390
|
+
const boundSlides = new WeakSet();
|
|
1391
|
+
function bindSlideClicks() {
|
|
1392
|
+
slides.forEach((slide) => {
|
|
1393
|
+
if (boundSlides.has(slide)) return;
|
|
1394
|
+
boundSlides.add(slide);
|
|
1395
|
+
slide.addEventListener("click", () => {
|
|
1396
|
+
if (!document.body.hasAttribute("data-overview-mode")) return;
|
|
1397
|
+
document.body.removeAttribute("data-overview-mode");
|
|
1398
|
+
show(slides.indexOf(slide));
|
|
1399
|
+
});
|
|
1204
1400
|
});
|
|
1205
|
-
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function refresh() {
|
|
1404
|
+
transitionToken += 1;
|
|
1405
|
+
isTransitioning = false;
|
|
1406
|
+
pendingNavigation = null;
|
|
1407
|
+
slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
|
|
1408
|
+
bindSlideClicks();
|
|
1409
|
+
if (document.body.hasAttribute("data-overview-mode")) {
|
|
1410
|
+
slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
|
|
1411
|
+
setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
|
|
1412
|
+
index = Math.max(0, Math.min(slides.length - 1, index));
|
|
1413
|
+
stepCount = fireCountForSlide(index);
|
|
1414
|
+
stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
|
|
1415
|
+
publishState();
|
|
1416
|
+
} else {
|
|
1417
|
+
applyInstantSlideChange(index, stepIndex);
|
|
1418
|
+
}
|
|
1419
|
+
document.querySelectorAll(".speaker-notes").forEach((note) => {
|
|
1420
|
+
note.hidden = !document.body.hasAttribute("data-presenter-mode");
|
|
1421
|
+
});
|
|
1422
|
+
fitDeck();
|
|
1423
|
+
}
|
|
1424
|
+
bindSlideClicks();
|
|
1206
1425
|
|
|
1207
1426
|
document.addEventListener("keydown", (event) => {
|
|
1208
1427
|
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
@@ -1225,7 +1444,7 @@ function renderPresentationScript(nonce) {
|
|
|
1225
1444
|
handleCommand(message.action, message.index, message.stepIndex);
|
|
1226
1445
|
});
|
|
1227
1446
|
|
|
1228
|
-
window.__honoDecksPresentationRuntime = { command: handleCommand };
|
|
1447
|
+
window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
|
|
1229
1448
|
|
|
1230
1449
|
window.addEventListener("resize", fitDeck);
|
|
1231
1450
|
if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
|
|
@@ -1261,7 +1480,7 @@ function renderCompiledDeckPage(input) {
|
|
|
1261
1480
|
<meta charset="utf-8" />
|
|
1262
1481
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1263
1482
|
<title>${escapeHtml$5(presentationPageTitle(deck))}</title>
|
|
1264
|
-
<style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
1483
|
+
<style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
1265
1484
|
${document.head}
|
|
1266
1485
|
</head>
|
|
1267
1486
|
<body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
|
|
@@ -1292,7 +1511,7 @@ async function renderCompiledDeckPageAsync(input) {
|
|
|
1292
1511
|
<meta charset="utf-8" />
|
|
1293
1512
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1294
1513
|
<title>${escapeHtml$5(presentationPageTitle(deck))}</title>
|
|
1295
|
-
<style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
1514
|
+
<style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
1296
1515
|
${document.head}
|
|
1297
1516
|
</head>
|
|
1298
1517
|
<body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
|
package/dist/bin.js
CHANGED
|
@@ -1660,8 +1660,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
|
|
|
1660
1660
|
function emitMarkdownClient(islands, runtime) {
|
|
1661
1661
|
if (!islands.length) return "";
|
|
1662
1662
|
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1663
|
+
const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
|
|
1664
|
+
const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
|
|
1665
|
+
const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
|
|
1666
|
+
const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
|
|
1663
1667
|
return `import { hydrateMarkdownIslands } from ${literal(runtime)};
|
|
1668
|
+
import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
|
|
1669
|
+
import { MarkdownContext } from ${literal(contextModule)};
|
|
1670
|
+
import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
|
|
1671
|
+
import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
|
|
1672
|
+
function hydrateDeckMarkdownIslands() {
|
|
1664
1673
|
hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
|
|
1674
|
+
schedule(element, loader, props, strategy, mount) {
|
|
1675
|
+
return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
|
|
1676
|
+
const live = window.__honoDecksLive;
|
|
1677
|
+
if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
|
|
1678
|
+
const Component = module.default;
|
|
1679
|
+
function LiveMarkdownIsland(componentProps) {
|
|
1680
|
+
const [current, setCurrent] = useLiveState({
|
|
1681
|
+
props: componentProps,
|
|
1682
|
+
context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
|
|
1683
|
+
});
|
|
1684
|
+
const [active, setActive] = useLiveState(true);
|
|
1685
|
+
live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
|
|
1686
|
+
return active ? liveJsx(MarkdownContext.Provider, {
|
|
1687
|
+
value: current.context,
|
|
1688
|
+
children: liveJsx(Component, current.props),
|
|
1689
|
+
}) : null;
|
|
1690
|
+
}
|
|
1691
|
+
return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
|
|
1692
|
+
});
|
|
1693
|
+
},
|
|
1665
1694
|
ready(element, start) {
|
|
1666
1695
|
const slide = element.closest('section[data-slide-state]');
|
|
1667
1696
|
if (!slide || slide.dataset.slideState === 'active') { start(); return; }
|
|
@@ -1670,7 +1699,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
|
|
|
1670
1699
|
});
|
|
1671
1700
|
observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
|
|
1672
1701
|
}
|
|
1673
|
-
})
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
hydrateDeckMarkdownIslands();
|
|
1705
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
|
|
1674
1706
|
}
|
|
1675
1707
|
//#endregion
|
|
1676
1708
|
//#region src/node/markdown-adapter.ts
|
|
@@ -1719,11 +1751,15 @@ async function emitClientEntryModule(input) {
|
|
|
1719
1751
|
import { hydrateSlideIslands } from "hono-decks/client";
|
|
1720
1752
|
${imports.join("\n")}
|
|
1721
1753
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1754
|
+
function hydrateDeckIslands() {
|
|
1755
|
+
hydrateSlideIslands({
|
|
1756
|
+
components: {
|
|
1757
|
+
${registrations.join(",\n ")}
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
hydrateDeckIslands();
|
|
1762
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
|
|
1727
1763
|
`,
|
|
1728
1764
|
resolveDir: input.cwd,
|
|
1729
1765
|
sourcefile: "hono-decks-client-entry.tsx",
|
package/dist/cli.js
CHANGED
|
@@ -1659,8 +1659,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
|
|
|
1659
1659
|
function emitMarkdownClient(islands, runtime) {
|
|
1660
1660
|
if (!islands.length) return "";
|
|
1661
1661
|
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1662
|
+
const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
|
|
1663
|
+
const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
|
|
1664
|
+
const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
|
|
1665
|
+
const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
|
|
1662
1666
|
return `import { hydrateMarkdownIslands } from ${literal(runtime)};
|
|
1667
|
+
import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
|
|
1668
|
+
import { MarkdownContext } from ${literal(contextModule)};
|
|
1669
|
+
import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
|
|
1670
|
+
import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
|
|
1671
|
+
function hydrateDeckMarkdownIslands() {
|
|
1663
1672
|
hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
|
|
1673
|
+
schedule(element, loader, props, strategy, mount) {
|
|
1674
|
+
return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
|
|
1675
|
+
const live = window.__honoDecksLive;
|
|
1676
|
+
if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
|
|
1677
|
+
const Component = module.default;
|
|
1678
|
+
function LiveMarkdownIsland(componentProps) {
|
|
1679
|
+
const [current, setCurrent] = useLiveState({
|
|
1680
|
+
props: componentProps,
|
|
1681
|
+
context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
|
|
1682
|
+
});
|
|
1683
|
+
const [active, setActive] = useLiveState(true);
|
|
1684
|
+
live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
|
|
1685
|
+
return active ? liveJsx(MarkdownContext.Provider, {
|
|
1686
|
+
value: current.context,
|
|
1687
|
+
children: liveJsx(Component, current.props),
|
|
1688
|
+
}) : null;
|
|
1689
|
+
}
|
|
1690
|
+
return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
|
|
1691
|
+
});
|
|
1692
|
+
},
|
|
1664
1693
|
ready(element, start) {
|
|
1665
1694
|
const slide = element.closest('section[data-slide-state]');
|
|
1666
1695
|
if (!slide || slide.dataset.slideState === 'active') { start(); return; }
|
|
@@ -1669,7 +1698,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
|
|
|
1669
1698
|
});
|
|
1670
1699
|
observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
|
|
1671
1700
|
}
|
|
1672
|
-
})
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
hydrateDeckMarkdownIslands();
|
|
1704
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
|
|
1673
1705
|
}
|
|
1674
1706
|
//#endregion
|
|
1675
1707
|
//#region src/node/markdown-adapter.ts
|
|
@@ -1718,11 +1750,15 @@ async function emitClientEntryModule(input) {
|
|
|
1718
1750
|
import { hydrateSlideIslands } from "hono-decks/client";
|
|
1719
1751
|
${imports.join("\n")}
|
|
1720
1752
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1753
|
+
function hydrateDeckIslands() {
|
|
1754
|
+
hydrateSlideIslands({
|
|
1755
|
+
components: {
|
|
1756
|
+
${registrations.join(",\n ")}
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
hydrateDeckIslands();
|
|
1761
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
|
|
1726
1762
|
`,
|
|
1727
1763
|
resolveDir: input.cwd,
|
|
1728
1764
|
sourcefile: "hono-decks-client-entry.tsx",
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render } from "hono/jsx/dom";
|
|
1
|
+
import { flushSync, render, useState } from "hono/jsx/dom";
|
|
2
2
|
import { jsx } from "hono/jsx/dom/jsx-runtime";
|
|
3
3
|
//#region src/client/islands.ts
|
|
4
4
|
function hydrateSlideIslands(input) {
|
|
@@ -8,7 +8,19 @@ function hydrateSlideIslands(input) {
|
|
|
8
8
|
if (!name) continue;
|
|
9
9
|
const component = input.components[name];
|
|
10
10
|
if (!component) continue;
|
|
11
|
-
|
|
11
|
+
const props = parseIslandProps(island.dataset.honoDecksProps);
|
|
12
|
+
const live = window.__honoDecksLive;
|
|
13
|
+
if (live) {
|
|
14
|
+
if (island.dataset.honoDecksLiveMounted) continue;
|
|
15
|
+
function LiveIsland() {
|
|
16
|
+
const [currentProps, setCurrentProps] = useState(props);
|
|
17
|
+
const [active, setActive] = useState(true);
|
|
18
|
+
live.register(island, setCurrentProps, () => flushSync(() => setActive(false)));
|
|
19
|
+
return active ? jsx(component, currentProps) : null;
|
|
20
|
+
}
|
|
21
|
+
render(jsx(LiveIsland, {}), island);
|
|
22
|
+
island.dataset.honoDecksLiveMounted = "true";
|
|
23
|
+
} else render(jsx(component, props), island);
|
|
12
24
|
}
|
|
13
25
|
}
|
|
14
26
|
function parseIslandProps(value) {
|
package/dist/node.js
CHANGED
|
@@ -1852,8 +1852,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
|
|
|
1852
1852
|
function emitMarkdownClient(islands, runtime) {
|
|
1853
1853
|
if (!islands.length) return "";
|
|
1854
1854
|
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1855
|
+
const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
|
|
1856
|
+
const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
|
|
1857
|
+
const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
|
|
1858
|
+
const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
|
|
1855
1859
|
return `import { hydrateMarkdownIslands } from ${literal(runtime)};
|
|
1860
|
+
import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
|
|
1861
|
+
import { MarkdownContext } from ${literal(contextModule)};
|
|
1862
|
+
import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
|
|
1863
|
+
import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
|
|
1864
|
+
function hydrateDeckMarkdownIslands() {
|
|
1856
1865
|
hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
|
|
1866
|
+
schedule(element, loader, props, strategy, mount) {
|
|
1867
|
+
return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
|
|
1868
|
+
const live = window.__honoDecksLive;
|
|
1869
|
+
if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
|
|
1870
|
+
const Component = module.default;
|
|
1871
|
+
function LiveMarkdownIsland(componentProps) {
|
|
1872
|
+
const [current, setCurrent] = useLiveState({
|
|
1873
|
+
props: componentProps,
|
|
1874
|
+
context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
|
|
1875
|
+
});
|
|
1876
|
+
const [active, setActive] = useLiveState(true);
|
|
1877
|
+
live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
|
|
1878
|
+
return active ? liveJsx(MarkdownContext.Provider, {
|
|
1879
|
+
value: current.context,
|
|
1880
|
+
children: liveJsx(Component, current.props),
|
|
1881
|
+
}) : null;
|
|
1882
|
+
}
|
|
1883
|
+
return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
|
|
1884
|
+
});
|
|
1885
|
+
},
|
|
1857
1886
|
ready(element, start) {
|
|
1858
1887
|
const slide = element.closest('section[data-slide-state]');
|
|
1859
1888
|
if (!slide || slide.dataset.slideState === 'active') { start(); return; }
|
|
@@ -1862,7 +1891,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
|
|
|
1862
1891
|
});
|
|
1863
1892
|
observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
|
|
1864
1893
|
}
|
|
1865
|
-
})
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
hydrateDeckMarkdownIslands();
|
|
1897
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
|
|
1866
1898
|
}
|
|
1867
1899
|
//#endregion
|
|
1868
1900
|
//#region src/node/markdown-adapter.ts
|
|
@@ -2001,11 +2033,15 @@ async function emitClientEntryModule(input) {
|
|
|
2001
2033
|
import { hydrateSlideIslands } from "hono-decks/client";
|
|
2002
2034
|
${imports.join("\n")}
|
|
2003
2035
|
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2036
|
+
function hydrateDeckIslands() {
|
|
2037
|
+
hydrateSlideIslands({
|
|
2038
|
+
components: {
|
|
2039
|
+
${registrations.join(",\n ")}
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
hydrateDeckIslands();
|
|
2044
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
|
|
2009
2045
|
`,
|
|
2010
2046
|
resolveDir: input.cwd,
|
|
2011
2047
|
sourcefile: "hono-decks-client-entry.tsx",
|
|
@@ -3523,19 +3559,209 @@ function escapeHtml$8(value) {
|
|
|
3523
3559
|
function renderLiveReloadScript(eventsPath, nonce) {
|
|
3524
3560
|
return `<script${documentNonceAttribute(nonce)}>
|
|
3525
3561
|
(() => {
|
|
3526
|
-
const
|
|
3527
|
-
const
|
|
3528
|
-
|
|
3562
|
+
const events = new EventSource(${JSON.stringify(eventsPath)});
|
|
3563
|
+
const scriptNonce = document.currentScript?.nonce;
|
|
3564
|
+
let running = false;
|
|
3565
|
+
let pending = false;
|
|
3566
|
+
const selector = "[data-hono-decks-island],[data-hono-md-island]";
|
|
3567
|
+
const islandUpdaters = new WeakMap();
|
|
3568
|
+
const hydrators = new Set();
|
|
3569
|
+
window.__honoDecksLive = {
|
|
3570
|
+
register: (element, update, dispose) => islandUpdaters.set(element, { update, dispose }),
|
|
3571
|
+
registerHydrator: (hydrate) => hydrators.add(hydrate),
|
|
3572
|
+
};
|
|
3573
|
+
const clientSelector = "script[data-hono-decks-client-entry]";
|
|
3574
|
+
const clientText = async (url) => {
|
|
3575
|
+
if (!url) return "";
|
|
3576
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
3577
|
+
if (!response.ok) throw new Error("Client entry fetch failed");
|
|
3578
|
+
return response.text();
|
|
3579
|
+
};
|
|
3580
|
+
const domReady = document.readyState === "loading"
|
|
3581
|
+
? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
|
|
3582
|
+
: Promise.resolve();
|
|
3583
|
+
let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
|
|
3584
|
+
|
|
3585
|
+
function signature(element, includeContext = true, includeProps = true) {
|
|
3586
|
+
const attributes = element.hasAttribute("data-hono-md-island")
|
|
3587
|
+
? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
|
|
3588
|
+
: ["data-hono-decks-island", "data-hono-decks-props"];
|
|
3589
|
+
return JSON.stringify([attributes.map((name) => !includeProps && name.endsWith("-props") ? null : element.getAttribute(name)),
|
|
3590
|
+
includeContext ? element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null : null]);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
async function update() {
|
|
3594
|
+
const response = await fetch(location.href, { cache: "no-store" });
|
|
3595
|
+
if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
|
|
3596
|
+
const next = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
3597
|
+
const currentDeck = document.querySelector("[data-hono-decks-deck]");
|
|
3598
|
+
const nextDeck = next.querySelector("[data-hono-decks-deck]");
|
|
3599
|
+
const style = document.querySelector("[data-hono-decks-presentation-style]");
|
|
3600
|
+
const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
|
|
3601
|
+
const runtime = window.__honoDecksPresentationRuntime;
|
|
3602
|
+
if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
|
|
3603
|
+
const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
|
|
3604
|
+
const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
|
|
3605
|
+
if (!newSlides.length) throw new Error("Empty deck");
|
|
3606
|
+
// Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
|
|
3607
|
+
if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
|
|
3608
|
+
const collect = (slides) => slides.flatMap((slide, slideIndex) =>
|
|
3609
|
+
Array.from(slide.querySelectorAll(selector)).map((element, position) => ({ element, slideIndex, position })));
|
|
3610
|
+
const oldItems = collect(oldSlides);
|
|
3611
|
+
const newItems = collect(newSlides);
|
|
3612
|
+
const nextScript = next.querySelector(clientSelector);
|
|
3613
|
+
const fetchedClient = await clientText(nextScript?.getAttribute("src"));
|
|
3614
|
+
const previousClient = await clientSnapshot;
|
|
3615
|
+
if (previousClient === null) throw new Error("Initial client entry fetch failed");
|
|
3616
|
+
// Keep the loaded registry when the last island disappears; it can dispose
|
|
3617
|
+
// that island and initialize it again if it is reintroduced later.
|
|
3618
|
+
const nextClient = !nextScript && !newItems.length ? previousClient : fetchedClient;
|
|
3619
|
+
// A changed client bundle could contain new implementations for running islands.
|
|
3620
|
+
if (oldItems.length && previousClient !== nextClient) throw new Error("Client code changed");
|
|
3621
|
+
const needsEntry = !document.querySelector(clientSelector) && Boolean(nextScript);
|
|
3622
|
+
if (newItems.length && !nextScript) throw new Error("New islands require a client entry");
|
|
3623
|
+
if (!needsEntry && previousClient !== nextClient) throw new Error("Client code changed");
|
|
3624
|
+
|
|
3625
|
+
const duplicateKeys = new Set([...oldItems, ...newItems].map((item) => signature(item.element, false)));
|
|
3626
|
+
for (const key of duplicateKeys) {
|
|
3627
|
+
const before = oldItems.filter((item) => signature(item.element, false) === key);
|
|
3628
|
+
const after = newItems.filter((item) => signature(item.element, false) === key);
|
|
3629
|
+
if (before.length <= 1 && after.length <= 1) continue;
|
|
3630
|
+
if (before.length !== after.length || before.some((item, index) =>
|
|
3631
|
+
item.slideIndex !== after[index].slideIndex || item.position !== after[index].position))
|
|
3632
|
+
throw new Error("Ambiguous duplicate islands");
|
|
3633
|
+
}
|
|
3634
|
+
const pairs = [];
|
|
3635
|
+
const remainingOld = new Set(oldItems);
|
|
3636
|
+
const remainingNew = new Set(newItems);
|
|
3637
|
+
function pair(oldItem, newItem) {
|
|
3638
|
+
remainingOld.delete(oldItem);
|
|
3639
|
+
remainingNew.delete(newItem);
|
|
3640
|
+
pairs.push([oldItem, newItem]);
|
|
3641
|
+
}
|
|
3642
|
+
// Exact matches can move between slides. Match only unique instances so
|
|
3643
|
+
// duplicate islands cannot accidentally inherit each other's state.
|
|
3644
|
+
for (const oldItem of oldItems) {
|
|
3645
|
+
const key = signature(oldItem.element, false);
|
|
3646
|
+
const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false) === key);
|
|
3647
|
+
const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false) === key);
|
|
3648
|
+
if (oldMatches.length === 1 && newMatches.length === 1) pair(oldItem, newMatches[0]);
|
|
3649
|
+
}
|
|
3650
|
+
// Changed props can be reconciled if there is only one island of that type.
|
|
3651
|
+
for (const oldItem of Array.from(remainingOld)) {
|
|
3652
|
+
const key = signature(oldItem.element, false, false);
|
|
3653
|
+
const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false, false) === key);
|
|
3654
|
+
const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false, false) === key);
|
|
3655
|
+
if (oldMatches.length === 1 && newMatches.length === 1 &&
|
|
3656
|
+
oldItem.slideIndex === newMatches[0].slideIndex && oldItem.position === newMatches[0].position)
|
|
3657
|
+
pair(oldItem, newMatches[0]);
|
|
3658
|
+
}
|
|
3659
|
+
// Existing duplicate instances remain safe only when their positions and
|
|
3660
|
+
// identity attributes are unchanged; ambiguous structural edits reload.
|
|
3661
|
+
for (const oldItem of Array.from(remainingOld)) {
|
|
3662
|
+
const newItem = Array.from(remainingNew).find((item) => item.slideIndex === oldItem.slideIndex && item.position === oldItem.position);
|
|
3663
|
+
if (newItem && signature(oldItem.element, false) === signature(newItem.element, false)) pair(oldItem, newItem);
|
|
3664
|
+
}
|
|
3665
|
+
for (const oldItem of remainingOld) {
|
|
3666
|
+
const key = signature(oldItem.element, false, false);
|
|
3667
|
+
if (Array.from(remainingNew).some((item) => signature(item.element, false, false) === key))
|
|
3668
|
+
throw new Error("Ambiguous island identity");
|
|
3669
|
+
}
|
|
3670
|
+
const removals = [];
|
|
3671
|
+
for (const oldItem of remainingOld) {
|
|
3672
|
+
const entry = islandUpdaters.get(oldItem.element);
|
|
3673
|
+
if (!entry?.dispose) throw new Error("Cannot safely dispose island");
|
|
3674
|
+
removals.push(entry.dispose);
|
|
3675
|
+
}
|
|
3676
|
+
const updates = [];
|
|
3677
|
+
let preserveSlides = false;
|
|
3678
|
+
for (const [oldItem, newItem] of pairs) {
|
|
3679
|
+
const before = oldItem.element;
|
|
3680
|
+
const after = newItem.element;
|
|
3681
|
+
const pendingMarkdown = before.hasAttribute("data-hono-md-island") && !before.hasAttribute("data-hono-md-mounted");
|
|
3682
|
+
if (pendingMarkdown) {
|
|
3683
|
+
if (oldSlides.length !== newSlides.length || oldItem.slideIndex !== newItem.slideIndex)
|
|
3684
|
+
throw new Error("Pending island moved between slides");
|
|
3685
|
+
preserveSlides = true;
|
|
3686
|
+
}
|
|
3687
|
+
if (signature(before, !pendingMarkdown) !== signature(after, !pendingMarkdown)) {
|
|
3688
|
+
const entry = islandUpdaters.get(before);
|
|
3689
|
+
if (!entry?.update || signature(before, false, false) !== signature(after, false, false))
|
|
3690
|
+
throw new Error("Island properties changed");
|
|
3691
|
+
const markdown = before.hasAttribute("data-hono-md-island");
|
|
3692
|
+
const propsAttribute = markdown ? "data-hono-md-props" : "data-hono-decks-props";
|
|
3693
|
+
const rawProps = after.getAttribute(propsAttribute) || "{}";
|
|
3694
|
+
const props = JSON.parse(rawProps);
|
|
3695
|
+
const context = markdown ? JSON.parse(after.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") || "{}") : undefined;
|
|
3696
|
+
updates.push([before, entry.update, propsAttribute, rawProps, markdown ? { props, context } : props]);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
for (const dispose of removals) dispose();
|
|
3700
|
+
for (const [oldItem, newItem] of pairs) newItem.element.replaceWith(oldItem.element);
|
|
3701
|
+
if (preserveSlides) {
|
|
3702
|
+
for (let i = 0; i < oldSlides.length; i++) {
|
|
3703
|
+
const oldSlide = oldSlides[i];
|
|
3704
|
+
const newSlide = newSlides[i];
|
|
3705
|
+
for (const attribute of Array.from(oldSlide.attributes)) oldSlide.removeAttribute(attribute.name);
|
|
3706
|
+
for (const attribute of Array.from(newSlide.attributes)) oldSlide.setAttribute(attribute.name, attribute.value);
|
|
3707
|
+
oldSlide.replaceChildren(...newSlide.childNodes);
|
|
3708
|
+
newSlide.replaceWith(oldSlide);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
currentDeck.replaceChildren(...nextDeck.childNodes);
|
|
3712
|
+
for (const [island, update, propsAttribute, rawProps, value] of updates) {
|
|
3713
|
+
island.setAttribute(propsAttribute, rawProps);
|
|
3714
|
+
update(value);
|
|
3715
|
+
}
|
|
3716
|
+
style.textContent = nextStyle.textContent;
|
|
3717
|
+
document.title = next.title;
|
|
3718
|
+
clientSnapshot = Promise.resolve(nextClient);
|
|
3719
|
+
runtime.refresh();
|
|
3720
|
+
if (remainingNew.size) {
|
|
3721
|
+
if (needsEntry) {
|
|
3722
|
+
const url = new URL(nextScript.getAttribute("src"), location.href);
|
|
3723
|
+
if (url.origin !== location.origin) throw new Error("Cross-origin client entry");
|
|
3724
|
+
url.searchParams.set("hono-decks-update", String(Date.now()));
|
|
3725
|
+
const script = document.createElement("script");
|
|
3726
|
+
script.type = "module";
|
|
3727
|
+
script.setAttribute("data-hono-decks-client-entry", "");
|
|
3728
|
+
if (scriptNonce) script.nonce = scriptNonce;
|
|
3729
|
+
script.src = url.href;
|
|
3730
|
+
await new Promise((resolve, reject) => {
|
|
3731
|
+
script.onload = resolve;
|
|
3732
|
+
script.onerror = () => reject(new Error("Client entry failed"));
|
|
3733
|
+
document.body.appendChild(script);
|
|
3734
|
+
});
|
|
3735
|
+
} else {
|
|
3736
|
+
for (const hydrate of hydrators) hydrate();
|
|
3737
|
+
}
|
|
3738
|
+
for (const item of remainingNew) {
|
|
3739
|
+
const element = item.element;
|
|
3740
|
+
if (!element.hasAttribute("data-hono-decks-live-mounted") && !element.hasAttribute("data-hono-md-scheduled"))
|
|
3741
|
+
throw new Error("New island did not initialize");
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
async function onUpdate() {
|
|
3747
|
+
if (running) { pending = true; return; }
|
|
3748
|
+
running = true;
|
|
3749
|
+
try {
|
|
3750
|
+
do { pending = false; await update(); } while (pending);
|
|
3751
|
+
} catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
|
|
3752
|
+
finally { running = false; }
|
|
3753
|
+
}
|
|
3754
|
+
events.addEventListener("deck:updated", onUpdate);
|
|
3529
3755
|
})();
|
|
3530
3756
|
<\/script>`;
|
|
3531
3757
|
}
|
|
3532
3758
|
function renderClientEntryScript(clientEntry, nonce) {
|
|
3533
|
-
return `<script type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
|
|
3759
|
+
return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
|
|
3534
3760
|
}
|
|
3535
3761
|
function renderPresentationScript(nonce) {
|
|
3536
3762
|
return `<script${documentNonceAttribute(nonce)}>
|
|
3537
3763
|
(() => {
|
|
3538
|
-
|
|
3764
|
+
let slides = Array.from(document.querySelectorAll(".slide"));
|
|
3539
3765
|
const stage = document.querySelector("[data-hono-decks-stage]");
|
|
3540
3766
|
const deck = document.querySelector("[data-hono-decks-deck]");
|
|
3541
3767
|
const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
|
|
@@ -3809,6 +4035,7 @@ function renderPresentationScript(nonce) {
|
|
|
3809
4035
|
updateFires(nextStepIndex);
|
|
3810
4036
|
publishState();
|
|
3811
4037
|
requestAnimationFrame(() => {
|
|
4038
|
+
if (token !== transitionToken) return;
|
|
3812
4039
|
setSlideState(outgoing, "leaving", direction, transition, timing);
|
|
3813
4040
|
setSlideState(incoming, "active", direction, transition, timing);
|
|
3814
4041
|
waitForSlideTransition(outgoing, incoming, direction, token);
|
|
@@ -3915,13 +4142,41 @@ function renderPresentationScript(nonce) {
|
|
|
3915
4142
|
await document.documentElement.requestFullscreen?.();
|
|
3916
4143
|
}
|
|
3917
4144
|
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4145
|
+
const boundSlides = new WeakSet();
|
|
4146
|
+
function bindSlideClicks() {
|
|
4147
|
+
slides.forEach((slide) => {
|
|
4148
|
+
if (boundSlides.has(slide)) return;
|
|
4149
|
+
boundSlides.add(slide);
|
|
4150
|
+
slide.addEventListener("click", () => {
|
|
4151
|
+
if (!document.body.hasAttribute("data-overview-mode")) return;
|
|
4152
|
+
document.body.removeAttribute("data-overview-mode");
|
|
4153
|
+
show(slides.indexOf(slide));
|
|
4154
|
+
});
|
|
3923
4155
|
});
|
|
3924
|
-
}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
function refresh() {
|
|
4159
|
+
transitionToken += 1;
|
|
4160
|
+
isTransitioning = false;
|
|
4161
|
+
pendingNavigation = null;
|
|
4162
|
+
slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
|
|
4163
|
+
bindSlideClicks();
|
|
4164
|
+
if (document.body.hasAttribute("data-overview-mode")) {
|
|
4165
|
+
slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
|
|
4166
|
+
setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
|
|
4167
|
+
index = Math.max(0, Math.min(slides.length - 1, index));
|
|
4168
|
+
stepCount = fireCountForSlide(index);
|
|
4169
|
+
stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
|
|
4170
|
+
publishState();
|
|
4171
|
+
} else {
|
|
4172
|
+
applyInstantSlideChange(index, stepIndex);
|
|
4173
|
+
}
|
|
4174
|
+
document.querySelectorAll(".speaker-notes").forEach((note) => {
|
|
4175
|
+
note.hidden = !document.body.hasAttribute("data-presenter-mode");
|
|
4176
|
+
});
|
|
4177
|
+
fitDeck();
|
|
4178
|
+
}
|
|
4179
|
+
bindSlideClicks();
|
|
3925
4180
|
|
|
3926
4181
|
document.addEventListener("keydown", (event) => {
|
|
3927
4182
|
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
@@ -3944,7 +4199,7 @@ function renderPresentationScript(nonce) {
|
|
|
3944
4199
|
handleCommand(message.action, message.index, message.stepIndex);
|
|
3945
4200
|
});
|
|
3946
4201
|
|
|
3947
|
-
window.__honoDecksPresentationRuntime = { command: handleCommand };
|
|
4202
|
+
window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
|
|
3948
4203
|
|
|
3949
4204
|
window.addEventListener("resize", fitDeck);
|
|
3950
4205
|
if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
|
|
@@ -3980,7 +4235,7 @@ async function renderCompiledDeckPageAsync(input) {
|
|
|
3980
4235
|
<meta charset="utf-8" />
|
|
3981
4236
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3982
4237
|
<title>${escapeHtml$6(presentationPageTitle(deck))}</title>
|
|
3983
|
-
<style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
4238
|
+
<style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
|
|
3984
4239
|
${document.head}
|
|
3985
4240
|
</head>
|
|
3986
4241
|
<body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
|
package/dist/vite.js
CHANGED
|
@@ -1659,8 +1659,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
|
|
|
1659
1659
|
function emitMarkdownClient(islands, runtime) {
|
|
1660
1660
|
if (!islands.length) return "";
|
|
1661
1661
|
const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1662
|
+
const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
|
|
1663
|
+
const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
|
|
1664
|
+
const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
|
|
1665
|
+
const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
|
|
1662
1666
|
return `import { hydrateMarkdownIslands } from ${literal(runtime)};
|
|
1667
|
+
import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
|
|
1668
|
+
import { MarkdownContext } from ${literal(contextModule)};
|
|
1669
|
+
import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
|
|
1670
|
+
import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
|
|
1671
|
+
function hydrateDeckMarkdownIslands() {
|
|
1663
1672
|
hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
|
|
1673
|
+
schedule(element, loader, props, strategy, mount) {
|
|
1674
|
+
return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
|
|
1675
|
+
const live = window.__honoDecksLive;
|
|
1676
|
+
if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
|
|
1677
|
+
const Component = module.default;
|
|
1678
|
+
function LiveMarkdownIsland(componentProps) {
|
|
1679
|
+
const [current, setCurrent] = useLiveState({
|
|
1680
|
+
props: componentProps,
|
|
1681
|
+
context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
|
|
1682
|
+
});
|
|
1683
|
+
const [active, setActive] = useLiveState(true);
|
|
1684
|
+
live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
|
|
1685
|
+
return active ? liveJsx(MarkdownContext.Provider, {
|
|
1686
|
+
value: current.context,
|
|
1687
|
+
children: liveJsx(Component, current.props),
|
|
1688
|
+
}) : null;
|
|
1689
|
+
}
|
|
1690
|
+
return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
|
|
1691
|
+
});
|
|
1692
|
+
},
|
|
1664
1693
|
ready(element, start) {
|
|
1665
1694
|
const slide = element.closest('section[data-slide-state]');
|
|
1666
1695
|
if (!slide || slide.dataset.slideState === 'active') { start(); return; }
|
|
@@ -1669,7 +1698,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
|
|
|
1669
1698
|
});
|
|
1670
1699
|
observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
|
|
1671
1700
|
}
|
|
1672
|
-
})
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
hydrateDeckMarkdownIslands();
|
|
1704
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
|
|
1673
1705
|
}
|
|
1674
1706
|
//#endregion
|
|
1675
1707
|
//#region src/node/markdown-adapter.ts
|
|
@@ -1718,11 +1750,15 @@ async function emitClientEntryModule(input) {
|
|
|
1718
1750
|
import { hydrateSlideIslands } from "hono-decks/client";
|
|
1719
1751
|
${imports.join("\n")}
|
|
1720
1752
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
}
|
|
1753
|
+
function hydrateDeckIslands() {
|
|
1754
|
+
hydrateSlideIslands({
|
|
1755
|
+
components: {
|
|
1756
|
+
${registrations.join(",\n ")}
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
hydrateDeckIslands();
|
|
1761
|
+
if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
|
|
1726
1762
|
`,
|
|
1727
1763
|
resolveDir: input.cwd,
|
|
1728
1764
|
sourcefile: "hono-decks-client-entry.tsx",
|
|
@@ -2307,6 +2343,8 @@ function honoDecks(options = {}) {
|
|
|
2307
2343
|
let mountChanged = false;
|
|
2308
2344
|
let modules = /* @__PURE__ */ new Map();
|
|
2309
2345
|
let routerModulePath = "";
|
|
2346
|
+
const eventClients = /* @__PURE__ */ new Set();
|
|
2347
|
+
let eventsPath = "/@hono-decks/events";
|
|
2310
2348
|
const syncWatchedPaths = async () => {
|
|
2311
2349
|
if (!server) return;
|
|
2312
2350
|
const next = /* @__PURE__ */ new Set([
|
|
@@ -2374,7 +2412,7 @@ function honoDecks(options = {}) {
|
|
|
2374
2412
|
environment.moduleGraph.invalidateAll();
|
|
2375
2413
|
if (environment.name !== "client") environment.hot.send({ type: "full-reload" });
|
|
2376
2414
|
}
|
|
2377
|
-
|
|
2415
|
+
for (const client of eventClients) client.write("event: deck:updated\ndata: {}\n\n");
|
|
2378
2416
|
}
|
|
2379
2417
|
} finally {
|
|
2380
2418
|
compiling = false;
|
|
@@ -2422,8 +2460,9 @@ export { createDecks };
|
|
|
2422
2460
|
export const decks = createDecks(config);`;
|
|
2423
2461
|
if (!id.startsWith(MODULE_PREFIX)) return null;
|
|
2424
2462
|
const path = id.slice(MODULE_PREFIX.length);
|
|
2425
|
-
|
|
2463
|
+
let code = modules.get(path);
|
|
2426
2464
|
if (code === void 0) return null;
|
|
2465
|
+
if (path === routerModulePath && resolvedConfig?.command === "serve") code = code.replace("const generatedDecks = defineDecks({", `const generatedDecks = defineDecks({\n liveReloadPath: () => ${JSON.stringify(eventsPath)},`);
|
|
2427
2466
|
return (await transform(code, {
|
|
2428
2467
|
loader: "ts",
|
|
2429
2468
|
format: "esm",
|
|
@@ -2436,16 +2475,30 @@ export const decks = createDecks(config);`;
|
|
|
2436
2475
|
},
|
|
2437
2476
|
async configResolved(config) {
|
|
2438
2477
|
resolvedConfig = config;
|
|
2478
|
+
eventsPath = `${config.base?.startsWith("/") ? config.base.replace(/\/$/, "") : ""}/@hono-decks/events`;
|
|
2439
2479
|
projectRoot = config.resolve?.preserveSymlinks ? config.root : await realpath(config.root);
|
|
2440
2480
|
configPath = resolve(projectRoot, options.configFile ?? "hono-decks.config.ts");
|
|
2441
2481
|
await compile(true);
|
|
2442
2482
|
},
|
|
2443
2483
|
configureServer(devServer) {
|
|
2444
2484
|
server = devServer;
|
|
2485
|
+
devServer.middlewares.use((req, res, next) => {
|
|
2486
|
+
if (req.url?.split("?")[0] !== eventsPath || req.method !== "GET") return next();
|
|
2487
|
+
res.writeHead(200, {
|
|
2488
|
+
"Content-Type": "text/event-stream",
|
|
2489
|
+
"Cache-Control": "no-cache, no-transform",
|
|
2490
|
+
Connection: "keep-alive"
|
|
2491
|
+
});
|
|
2492
|
+
res.write(": connected\n\n");
|
|
2493
|
+
eventClients.add(res);
|
|
2494
|
+
res.on("close", () => eventClients.delete(res));
|
|
2495
|
+
});
|
|
2445
2496
|
syncWatchedPaths();
|
|
2446
2497
|
devServer.watcher.on("all", onWatcherEvent);
|
|
2447
2498
|
const cleanup = () => {
|
|
2448
2499
|
if (timer) clearTimeout(timer);
|
|
2500
|
+
for (const client of eventClients) client.end();
|
|
2501
|
+
eventClients.clear();
|
|
2449
2502
|
devServer.watcher.off("all", onWatcherEvent);
|
|
2450
2503
|
server = void 0;
|
|
2451
2504
|
watchedPaths.clear();
|