hono-decks 1.1.0 → 1.2.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 CHANGED
@@ -69,7 +69,7 @@ npm run dev
69
69
  npm run build
70
70
  ```
71
71
 
72
- この連携には `void-plugin-hono-jsx@^0.4.0` の standalone compiler/client export
72
+ この連携には `void-plugin-hono-jsx@^0.5.0` の standalone compiler/client export
73
73
  が必要です。`--install` は対応範囲のアダプターを選びます。
74
74
  `--install` を省略する場合は、Markdown 用の任意 peer も必要です:
75
75
  `markdown-exit@^1.3.0 yaml@^2.9.1 markdown-it-container@^4.0.0 shiki@^4.5.0 @shikijs/transformers@^4.5.0`。
@@ -474,3 +474,37 @@ app.route("/internal-slides", decksRouter({ source, dev: true }));
474
474
  反映します。反映中に失敗した場合は、直前の内容へ戻します。OS やプロセスの停止時の
475
475
  一括反映、複数プロセスの同時実行は保証しません。復元自体に失敗した場合は、
476
476
  復旧用ファイルを残し、その場所をエラーに表示します。
477
+
478
+ ## Markdownアダプター
479
+
480
+ 従来の `build.markdown: "void"` に加えて、ビルド時に読み込むアダプターを指定できます。
481
+
482
+ ```ts
483
+ export default defineDecksConfig({
484
+ mountPath: "/slides",
485
+ router: { clientEntryAssetPath: "/_assets/client" },
486
+ build: {
487
+ root: "decks",
488
+ markdown: { adapter: "hono-decks/adapters/hono-markdown" },
489
+ },
490
+ });
491
+ ```
492
+
493
+ このブリッジは共通の `@ts-76/hono-markdown` を利用します。共通パッケージはVoid・Viteに依存しません。このアダプターを選ぶ場合はコンパイラー用peer dependenciesも導入します。
494
+ 既存の `markdown: "void"` は、既に導入したVoidアダプターの公開コンパイラーAPIでも引き続き動作します。
495
+ `init --framework void --install` の導入手順、既存のdev/buildコマンドは変わりません。
496
+
497
+ 設定はサーバー実行時にも読み込むため、アダプターは文字列で指定し、Node用のfactoryを設定ファイルへimportしません。
498
+ Hono+Vite・HonoXは引き続きMDXが標準で、VoidやMarkdown用パーサーの追加は不要です。
499
+ これらでMarkdownを使う場合は、共通パッケージとコンパイラー用peer dependenciesを導入し、上記ブリッジを選択します。
500
+ Viteは仮想モジュールを利用し、既存の `hono-decks compile` はファイル出力を維持します。
501
+
502
+ 独自アダプターは `createMarkdownAdapter({ cwd, options })` をexportし、`read(source)`、非同期の
503
+ `compile(source, { sourcePath })`、`client(islands)` を返します。型は `hono-decks/adapters/hono-markdown` からimportできます。
504
+ スライド分割・メタデータ・アセット・ルーティング・表示中スライドの判定はhono-decksが担当し、Markdownの変換とislandのスケジュールはアダプターが担当します。
505
+ 同じVoidアプリにhonoxpressを組み込んでも、ドキュメントとスライドが互いの内部実装に依存する必要はありません。
506
+
507
+ Hono+Vite・HonoXのinitは、dev serverが標準で`.js`を除外するため、拡張子なしの
508
+ `/_assets/client`を設定します。配信は既存アプリのミドルウェア・認可を通ります。
509
+ 既存CLI構成の標準パス`/_assets/client.js`は維持します。VoidなしのMarkdown実アプリ検証は、
510
+ `HONO_MARKDOWN_PACKAGE`に共通パッケージのtarballを指定し、`node scripts/verify-vite-init.mjs`で実行できます。
package/README.md CHANGED
@@ -68,7 +68,7 @@ npm run dev
68
68
  npm run build
69
69
  ~~~
70
70
 
71
- This integration requires `void-plugin-hono-jsx@^0.4.0` or a compatible adapter
71
+ This integration requires `void-plugin-hono-jsx@^0.5.0` or a compatible adapter
72
72
  with the `/markdown/compiler` and `/markdown/client` exports. `--install` selects
73
73
  the supported adapter range. Without `--install`, also install the optional Markdown peers:
74
74
  `markdown-exit@^1.3.0 yaml@^2.9.1 markdown-it-container@^4.0.0 shiki@^4.5.0 @shikijs/transformers@^4.5.0`.
@@ -465,7 +465,7 @@ At this level, the application owns the mount path, source policy, and option me
465
465
  ## Void Hono Markdown
466
466
 
467
467
  Set `build.markdown: "void"` in `hono-decks.config.ts` to opt into `.md` decks
468
- alongside MDX. This requires `void-plugin-hono-jsx@^0.4.0` with its standalone compiler and client exports.
468
+ alongside MDX. This requires `void-plugin-hono-jsx@^0.5.0` with its standalone compiler and client exports.
469
469
  See the [Void consumer fixture](https://github.com/ts-76/hono-decks/tree/main/fixtures/void-markdown)
470
470
  for package verification, Void middleware hosting, supported syntax and limitations.
471
471
 
@@ -490,3 +490,47 @@ files (including cache files) on a publication error. Application-owned files
490
490
  are preserved. This guarantees rollback on reported build failure, not
491
491
  crash-atomic publication or coordination between concurrent compiler processes.
492
492
  If rollback itself fails, the error lists retained recovery directories.
493
+
494
+ ## Markdown adapters
495
+
496
+ The deck compiler accepts a build-only module reference as well as the existing
497
+ `build.markdown: "void"` preset:
498
+
499
+ ```ts
500
+ export default defineDecksConfig({
501
+ mountPath: "/slides",
502
+ router: { clientEntryAssetPath: "/_assets/client" },
503
+ build: {
504
+ root: "decks",
505
+ markdown: { adapter: "hono-decks/adapters/hono-markdown" },
506
+ },
507
+ });
508
+ ```
509
+
510
+ The Hono Markdown bridge uses `@ts-76/hono-markdown/compiler` and its browser
511
+ runtime. The common package has no Void or Vite dependency. Install its optional compiler peers when selecting this adapter. Existing
512
+ Void configurations continue to work with the adapter's public compiler API,
513
+ including compatible earlier adapter versions. `init --framework void
514
+ --install` retains the one-command setup and unchanged dev/build scripts.
515
+
516
+ Keep the adapter reference in config as a string: config is also imported by the
517
+ server runtime. The compiler loads adapters only during builds. Hono+Vite and
518
+ HonoX continue to default to MDX without installing Void or Markdown peers.
519
+ For Markdown in these hosts, install the common package and its compiler peers
520
+ and select the bridge above. Vite uses virtual modules; `hono-decks compile`
521
+ continues to emit files.
522
+
523
+ Custom build adapters export `createMarkdownAdapter({ cwd, options })`, returning
524
+ `read(source)`, asynchronous `compile(source, { sourcePath })`, and
525
+ `client(islands)`. Types are exported from `hono-decks/adapters/hono-markdown`.
526
+ The deck host owns splitting, metadata, assets, routing and the visible-slide
527
+ hydration gate; Markdown compilation and island scheduling belong to the adapter.
528
+ Document adapters such as honoxpress can use the same Markdown package in a Void
529
+ app without importing deck internals or changing their route ownership.
530
+
531
+ Hono+Vite/HonoX init selects the extensionless `/_assets/client` route because
532
+ `@hono/vite-dev-server` excludes `.js` requests by default. The client still goes
533
+ through the application's middleware and authorization. Existing standalone CLI
534
+ configurations retain their default `/_assets/client.js` route. For local
535
+ Markdown checks without Void, set `HONO_MARKDOWN_PACKAGE` to the common tarball
536
+ and run `node scripts/verify-vite-init.mjs`.
@@ -0,0 +1,39 @@
1
+ import "hono";
2
+ import "hono/jsx";
3
+ import "hono/utils/html";
4
+ //#region src/node/markdown-adapter.d.ts
5
+ interface MarkdownIsland {
6
+ id: string;
7
+ modulePath: string;
8
+ hydrate: string;
9
+ }
10
+ interface DeckMarkdownAdapter {
11
+ read(source: string): {
12
+ frontmatter: Record<string, unknown>;
13
+ content: string;
14
+ script: string;
15
+ };
16
+ compile(source: string, input: {
17
+ sourcePath: string;
18
+ }): Promise<{
19
+ code: string;
20
+ headings: {
21
+ depth: number;
22
+ slug: string;
23
+ text: string;
24
+ }[];
25
+ clientCode: string;
26
+ islands?: MarkdownIsland[];
27
+ }>;
28
+ client(islands: MarkdownIsland[]): string;
29
+ }
30
+ //#endregion
31
+ //#region src/adapters/hono-markdown.d.ts
32
+ /** Node-only bridge. Existing Void configs retain their original compiler/runtime pair. */
33
+ declare function createHonoMarkdownAdapter(cwd: string, legacy?: boolean, options?: Record<string, unknown>): Promise<DeckMarkdownAdapter>;
34
+ declare function createMarkdownAdapter({ cwd, options }: {
35
+ cwd: string;
36
+ options?: Record<string, unknown>;
37
+ }): Promise<DeckMarkdownAdapter>;
38
+ //#endregion
39
+ export { type DeckMarkdownAdapter, type MarkdownIsland, createHonoMarkdownAdapter, createMarkdownAdapter };
@@ -0,0 +1,47 @@
1
+ import { createRequire } from "node:module";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/adapters/hono-markdown.ts
5
+ /** Node-only bridge. Existing Void configs retain their original compiler/runtime pair. */
6
+ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
7
+ const require = createRequire(resolve(cwd, "package.json"));
8
+ const entry = legacy ? "void-plugin-hono-jsx/markdown/compiler" : "@ts-76/hono-markdown/compiler";
9
+ let compiler;
10
+ try {
11
+ compiler = await import(pathToFileURL(require.resolve(entry)).href);
12
+ } catch (cause) {
13
+ throw new Error(`Hono Markdown requires ${legacy ? "its public /markdown/compiler API" : "@ts-76/hono-markdown/compiler"} and Markdown peers installed in the app.`, { cause });
14
+ }
15
+ const read = compiler.readMarkdownModule ?? compiler.readStaticMarkdown;
16
+ const compile = compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown;
17
+ if (typeof read !== "function" || typeof compile !== "function") throw new Error("Missing standalone compiler API");
18
+ const runtime = entry.startsWith("void-plugin") ? "void-plugin-hono-jsx/markdown/client" : "@ts-76/hono-markdown/client";
19
+ return {
20
+ read,
21
+ compile: (source, input) => compile(source, {
22
+ ...input,
23
+ options
24
+ }),
25
+ client: (islands) => emitMarkdownClient(islands, runtime)
26
+ };
27
+ }
28
+ function createMarkdownAdapter({ cwd, options }) {
29
+ return createHonoMarkdownAdapter(cwd, false, options);
30
+ }
31
+ function emitMarkdownClient(islands, runtime) {
32
+ if (!islands.length) return "";
33
+ const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
34
+ return `import { hydrateMarkdownIslands } from ${literal(runtime)};
35
+ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
36
+ ready(element, start) {
37
+ const slide = element.closest('section[data-slide-state]');
38
+ if (!slide || slide.dataset.slideState === 'active') { start(); return; }
39
+ const observer = new MutationObserver(() => {
40
+ if (slide.dataset.slideState === 'active') { observer.disconnect(); start(); }
41
+ });
42
+ observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
43
+ }
44
+ });`;
45
+ }
46
+ //#endregion
47
+ export { createHonoMarkdownAdapter, createMarkdownAdapter };
@@ -544,8 +544,11 @@ interface DefinedDecks<E extends Env = any> {
544
544
  }
545
545
  /** File-system settings consumed by the CLI. */
546
546
  interface DecksBuildConfig {
547
- /** Opt into .md decks compiled by void-plugin-hono-jsx/markdown/compiler. */
548
- markdown?: "void";
547
+ /** Build-only Markdown adapter reference. "void" retains the compatible preset. */
548
+ markdown?: "void" | {
549
+ adapter: string;
550
+ options?: Record<string, unknown>;
551
+ };
549
552
  /** Deck source directory. @default "decks" */
550
553
  root?: string;
551
554
  /** CLI output directory. Vite keeps modules in memory. @default "src/generated" */
package/dist/bin.js CHANGED
@@ -8,10 +8,10 @@ import { unified } from "unified";
8
8
  import remarkGfm from "remark-gfm";
9
9
  import remarkMdx from "remark-mdx";
10
10
  import remarkParse from "remark-parse";
11
- import { fileURLToPath, pathToFileURL } from "node:url";
12
11
  import { compile } from "@mdx-js/mdx";
13
12
  import remarkDirective from "remark-directive";
14
13
  import { codeToHtml } from "shiki";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
15
  import { build } from "esbuild";
16
16
  import { lookup } from "node:dns/promises";
17
17
  import { isIP } from "node:net";
@@ -1506,29 +1506,20 @@ function extractDeckPrelude(source) {
1506
1506
  };
1507
1507
  }
1508
1508
  //#endregion
1509
- //#region src/node/void-markdown.ts
1510
- /** Load only for an explicit Void Markdown build; ordinary MDX has no new peers. */
1511
- async function compileVoidMarkdownDecks(input) {
1509
+ //#region src/node/markdown-decks.ts
1510
+ async function compileMarkdownDecks(input, adapter) {
1512
1511
  const markdownEntries = input.decks.filter((entry) => entry.sourcePath.endsWith(".md"));
1513
1512
  const mdx = await compileMdxModuleDecks({
1514
1513
  ...input,
1515
1514
  decks: input.decks.filter((entry) => !entry.sourcePath.endsWith(".md"))
1516
1515
  });
1517
1516
  if (!markdownEntries.length) return mdx;
1518
- let compiler;
1519
- try {
1520
- compiler = await import(pathToFileURL(createRequire(resolve(input.cwd ?? process.cwd(), "package.json")).resolve("void-plugin-hono-jsx/markdown/compiler")).href);
1521
- if (typeof (compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown) !== "function" || typeof (compiler.readMarkdownModule ?? compiler.readStaticMarkdown) !== "function") throw new Error("Missing standalone compiler API");
1522
- } catch (error) {
1523
- throw new Error("Void Markdown requires void-plugin-hono-jsx with the public /markdown/compiler API and its Markdown peers installed in the app.", { cause: error });
1524
- }
1525
- const readModule = compiler.readMarkdownModule ?? compiler.readStaticMarkdown;
1526
- const compileModule = compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown;
1527
1517
  const decks = [...mdx.decks];
1528
1518
  const clients = /* @__PURE__ */ new Set();
1529
1519
  const markdownIslands = /* @__PURE__ */ new Map();
1530
1520
  for (const entry of markdownEntries) try {
1531
- const parsed = readModule(await input.readText(entry.sourcePath));
1521
+ const source = await input.readText(entry.sourcePath);
1522
+ const parsed = adapter.read(source);
1532
1523
  if (input.clientComponentIds?.[entry.slug] && Object.keys(input.clientComponentIds[entry.slug]).length) throw new Error("Void Markdown slides support static components only; use MDX for hono-decks client components.");
1533
1524
  const warnings = [];
1534
1525
  const meta = toDeckFrontmatter(parsed.frontmatter, warnings);
@@ -1549,12 +1540,13 @@ async function compileVoidMarkdownDecks(input) {
1549
1540
  const slides = [];
1550
1541
  for (const [index, slideSource] of splitMarkdownSlides(parsed.content).entries()) {
1551
1542
  const prelude = parsed.script ? `<script>\n${parsed.script}\n<\/script>\n` : "";
1552
- const slide = readModule(prelude + slideSource);
1543
+ const slide = adapter.read(prelude + slideSource);
1553
1544
  const path = `${input.outDir}/decks/${entry.slug}/slide-${index}.ts`;
1554
- const result = await compileModule(rewriteAssetUrls(`${prelude}---\n${JSON.stringify({
1545
+ const rewritten = rewriteAssetUrls(`${prelude}---\n${JSON.stringify({
1555
1546
  ...parsed.frontmatter,
1556
1547
  ...slide.frontmatter
1557
- })}\n---\n${slide.content}`, assets), { sourcePath: resolve(input.cwd ?? process.cwd(), entry.sourcePath) });
1548
+ })}\n---\n${slide.content}`, assets);
1549
+ const result = await adapter.compile(rewritten, { sourcePath: resolve(input.cwd ?? process.cwd(), entry.sourcePath) });
1558
1550
  clientEntryRequired ||= Boolean(result.clientCode || result.islands?.length);
1559
1551
  if (result.clientCode) clients.add(result.clientCode);
1560
1552
  for (const island of result.islands ?? []) markdownIslands.set(island.id, island);
@@ -1604,6 +1596,7 @@ async function compileVoidMarkdownDecks(input) {
1604
1596
  decks,
1605
1597
  routerModule: emitModuleDecksRouter({ decks }),
1606
1598
  clientCode: [...clients].join("\n"),
1599
+ clientModules: [adapter.client([...markdownIslands.values()])].filter(Boolean),
1607
1600
  markdownIslands: [...markdownIslands.values()]
1608
1601
  };
1609
1602
  }
@@ -1640,6 +1633,64 @@ function splitMarkdownSlides(source) {
1640
1633
  return splitSlideSources(protectedLines.join("\n")).map((slide) => slide.replace(new RegExp(`${prefix}(\\d+)_END`, "g"), (_, id) => fences[Number(id)]));
1641
1634
  }
1642
1635
  //#endregion
1636
+ //#region src/adapters/hono-markdown.ts
1637
+ /** Node-only bridge. Existing Void configs retain their original compiler/runtime pair. */
1638
+ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1639
+ const require = createRequire(resolve(cwd, "package.json"));
1640
+ const entry = legacy ? "void-plugin-hono-jsx/markdown/compiler" : "@ts-76/hono-markdown/compiler";
1641
+ let compiler;
1642
+ try {
1643
+ compiler = await import(pathToFileURL(require.resolve(entry)).href);
1644
+ } catch (cause) {
1645
+ throw new Error(`Hono Markdown requires ${legacy ? "its public /markdown/compiler API" : "@ts-76/hono-markdown/compiler"} and Markdown peers installed in the app.`, { cause });
1646
+ }
1647
+ const read = compiler.readMarkdownModule ?? compiler.readStaticMarkdown;
1648
+ const compile = compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown;
1649
+ if (typeof read !== "function" || typeof compile !== "function") throw new Error("Missing standalone compiler API");
1650
+ const runtime = entry.startsWith("void-plugin") ? "void-plugin-hono-jsx/markdown/client" : "@ts-76/hono-markdown/client";
1651
+ return {
1652
+ read,
1653
+ compile: (source, input) => compile(source, {
1654
+ ...input,
1655
+ options
1656
+ }),
1657
+ client: (islands) => emitMarkdownClient(islands, runtime)
1658
+ };
1659
+ }
1660
+ function emitMarkdownClient(islands, runtime) {
1661
+ if (!islands.length) return "";
1662
+ const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1663
+ return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1664
+ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1665
+ ready(element, start) {
1666
+ const slide = element.closest('section[data-slide-state]');
1667
+ if (!slide || slide.dataset.slideState === 'active') { start(); return; }
1668
+ const observer = new MutationObserver(() => {
1669
+ if (slide.dataset.slideState === 'active') { observer.disconnect(); start(); }
1670
+ });
1671
+ observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1672
+ }
1673
+ });`;
1674
+ }
1675
+ //#endregion
1676
+ //#region src/node/markdown-adapter.ts
1677
+ async function loadMarkdownAdapter(selection, cwd) {
1678
+ if (selection === "void") return createHonoMarkdownAdapter(cwd, true);
1679
+ if (selection.adapter === "hono-decks/adapters/hono-markdown") return createHonoMarkdownAdapter(cwd, false, selection.options);
1680
+ const module = await import(pathToFileURL(createRequire(resolve(cwd, "package.json")).resolve(selection.adapter)).href);
1681
+ if (typeof module.createMarkdownAdapter !== "function") throw new Error("Markdown adapter must export createMarkdownAdapter.");
1682
+ const adapter = await module.createMarkdownAdapter({
1683
+ cwd,
1684
+ options: selection.options
1685
+ });
1686
+ if (![
1687
+ adapter?.read,
1688
+ adapter?.compile,
1689
+ adapter?.client
1690
+ ].every((value) => typeof value === "function")) throw new Error("Markdown adapter must provide read, compile and client functions.");
1691
+ return adapter;
1692
+ }
1693
+ //#endregion
1643
1694
  //#region src/node/client-entry.ts
1644
1695
  async function discoverClientComponentIds(input) {
1645
1696
  const result = {};
@@ -1660,12 +1711,11 @@ async function emitClientEntryModule(input) {
1660
1711
  registrations.push(`${JSON.stringify(clientId)}: ${localName}`);
1661
1712
  }
1662
1713
  }
1663
- const markdown = input.markdownIslands ?? [];
1664
- if (registrations.length === 0 && markdown.length === 0) return `export const decksClientEntry = ${JSON.stringify(input.extraCode ?? "")};\n`;
1714
+ const clientModules = input.clientModules ?? [];
1715
+ if (registrations.length === 0 && clientModules.length === 0) return `export const decksClientEntry = ${JSON.stringify(input.extraCode ?? "")};\n`;
1665
1716
  const result = await build({
1666
1717
  stdin: {
1667
- contents: `${markdown.length ? `import { hydrateMarkdownIslands } from "void-plugin-hono-jsx/markdown/client";
1668
- hydrateMarkdownIslands({ ${markdown.map((entry) => `${javascriptString(entry.id)}: () => import(${javascriptString(entry.modulePath)})`).join(",")} });` : ""}
1718
+ contents: `${clientModules.join("\n")}
1669
1719
  import { hydrateSlideIslands } from "hono-decks/client";
1670
1720
  ${imports.join("\n")}
1671
1721
 
@@ -1742,9 +1792,6 @@ function hashString(value) {
1742
1792
  function safeIdentifier(value) {
1743
1793
  return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1744
1794
  }
1745
- function javascriptString(value) {
1746
- return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1747
- }
1748
1795
  //#endregion
1749
1796
  //#region src/node/local-deck-io.ts
1750
1797
  async function listFiles(cwd, dir) {
@@ -1952,7 +1999,7 @@ async function compileDeckModules(input) {
1952
1999
  const out = normalizeRelativePath(input.out, "Output directory");
1953
2000
  const root = normalizeDeckRoot(input.root);
1954
2001
  const paths = await listFiles(input.cwd, join(input.cwd, root));
1955
- const resolved = resolveDeckFiles(paths, root, { markdown: input.markdown === "void" });
2002
+ const resolved = resolveDeckFiles(paths, root, { markdown: Boolean(input.markdown) });
1956
2003
  const componentModulePaths = Object.fromEntries(resolved.flatMap((deck) => {
1957
2004
  const path = deck.kind === "directory" ? findComponentModule(paths, root, deck.slug) : void 0;
1958
2005
  return path ? [[deck.slug, path]] : [];
@@ -1980,7 +2027,8 @@ async function compileDeckModules(input) {
1980
2027
  refresh: input.refreshOgp,
1981
2028
  resolveOgp: input.resolveOgp ?? resolveOgpMetadata
1982
2029
  });
1983
- const generated = await (input.markdown === "void" ? compileVoidMarkdownDecks : compileMdxModuleDecks)({
2030
+ const adapter = input.markdown && resolved.some((entry) => entry.sourcePath.endsWith(".md")) ? await loadMarkdownAdapter(input.markdown, input.cwd) : void 0;
2031
+ const generated = await (adapter ? (value) => compileMarkdownDecks(value, adapter) : compileMdxModuleDecks)({
1984
2032
  cwd: input.cwd,
1985
2033
  root,
1986
2034
  outDir: out,
@@ -1999,7 +2047,7 @@ async function compileDeckModules(input) {
1999
2047
  clientEntries: clientEntryPaths,
2000
2048
  clientComponentIds,
2001
2049
  extraCode: generated.clientCode,
2002
- markdownIslands: generated.markdownIslands,
2050
+ clientModules: generated.clientModules,
2003
2051
  onCss: (css) => {
2004
2052
  clientCss = css;
2005
2053
  }
@@ -2149,7 +2197,7 @@ async function loadDecksConfig(input) {
2149
2197
  const config = (await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}#${Date.now()}`)).default;
2150
2198
  if (!config || typeof config !== "object") throw new Error(`Config must have a default export: ${path}`);
2151
2199
  if (typeof config.mountPath !== "string") throw new Error(`Config mountPath must be a string: ${path}`);
2152
- if (config.build?.markdown !== void 0 && config.build.markdown !== "void") throw new Error("Config build.markdown must be \"void\".");
2200
+ if (config.build?.markdown !== void 0 && config.build.markdown !== "void" && (typeof config.build.markdown !== "object" || !config.build.markdown || typeof config.build.markdown.adapter !== "string" || !config.build.markdown.adapter.trim())) throw new Error("Config build.markdown must be \"void\" or an adapter reference.");
2153
2201
  return {
2154
2202
  path,
2155
2203
  config,
@@ -2302,7 +2350,7 @@ async function runInitCommand(input, args, stdout, stderr) {
2302
2350
  return { exitCode: 1 };
2303
2351
  }
2304
2352
  if (parsed.options.framework) try {
2305
- const { initializeVite } = await import("./init-vite-mLg3XlcN.js");
2353
+ const { initializeVite } = await import("./init-vite-EA_L4k2H.js");
2306
2354
  const sampleSlug = await initializeVite(input.cwd, parsed.options.framework, parsed.options.configFile, parsed.options.entry, parsed.options.install ? { adapter: parsed.options.adapter } : void 0);
2307
2355
  stdout(`Initialized ${parsed.options.framework} decks at /slides/${sampleSlug}. Run your usual dev or build command; no manual compile is needed.`);
2308
2356
  return { exitCode: 0 };
package/dist/cli.js CHANGED
@@ -7,10 +7,10 @@ import { unified } from "unified";
7
7
  import remarkGfm from "remark-gfm";
8
8
  import remarkMdx from "remark-mdx";
9
9
  import remarkParse from "remark-parse";
10
- import { fileURLToPath, pathToFileURL } from "node:url";
11
10
  import { compile } from "@mdx-js/mdx";
12
11
  import remarkDirective from "remark-directive";
13
12
  import { codeToHtml } from "shiki";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import { build } from "esbuild";
15
15
  import { lookup } from "node:dns/promises";
16
16
  import { isIP } from "node:net";
@@ -1505,29 +1505,20 @@ function extractDeckPrelude(source) {
1505
1505
  };
1506
1506
  }
1507
1507
  //#endregion
1508
- //#region src/node/void-markdown.ts
1509
- /** Load only for an explicit Void Markdown build; ordinary MDX has no new peers. */
1510
- async function compileVoidMarkdownDecks(input) {
1508
+ //#region src/node/markdown-decks.ts
1509
+ async function compileMarkdownDecks(input, adapter) {
1511
1510
  const markdownEntries = input.decks.filter((entry) => entry.sourcePath.endsWith(".md"));
1512
1511
  const mdx = await compileMdxModuleDecks({
1513
1512
  ...input,
1514
1513
  decks: input.decks.filter((entry) => !entry.sourcePath.endsWith(".md"))
1515
1514
  });
1516
1515
  if (!markdownEntries.length) return mdx;
1517
- let compiler;
1518
- try {
1519
- compiler = await import(pathToFileURL(createRequire(resolve(input.cwd ?? process.cwd(), "package.json")).resolve("void-plugin-hono-jsx/markdown/compiler")).href);
1520
- if (typeof (compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown) !== "function" || typeof (compiler.readMarkdownModule ?? compiler.readStaticMarkdown) !== "function") throw new Error("Missing standalone compiler API");
1521
- } catch (error) {
1522
- throw new Error("Void Markdown requires void-plugin-hono-jsx with the public /markdown/compiler API and its Markdown peers installed in the app.", { cause: error });
1523
- }
1524
- const readModule = compiler.readMarkdownModule ?? compiler.readStaticMarkdown;
1525
- const compileModule = compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown;
1526
1516
  const decks = [...mdx.decks];
1527
1517
  const clients = /* @__PURE__ */ new Set();
1528
1518
  const markdownIslands = /* @__PURE__ */ new Map();
1529
1519
  for (const entry of markdownEntries) try {
1530
- const parsed = readModule(await input.readText(entry.sourcePath));
1520
+ const source = await input.readText(entry.sourcePath);
1521
+ const parsed = adapter.read(source);
1531
1522
  if (input.clientComponentIds?.[entry.slug] && Object.keys(input.clientComponentIds[entry.slug]).length) throw new Error("Void Markdown slides support static components only; use MDX for hono-decks client components.");
1532
1523
  const warnings = [];
1533
1524
  const meta = toDeckFrontmatter(parsed.frontmatter, warnings);
@@ -1548,12 +1539,13 @@ async function compileVoidMarkdownDecks(input) {
1548
1539
  const slides = [];
1549
1540
  for (const [index, slideSource] of splitMarkdownSlides(parsed.content).entries()) {
1550
1541
  const prelude = parsed.script ? `<script>\n${parsed.script}\n<\/script>\n` : "";
1551
- const slide = readModule(prelude + slideSource);
1542
+ const slide = adapter.read(prelude + slideSource);
1552
1543
  const path = `${input.outDir}/decks/${entry.slug}/slide-${index}.ts`;
1553
- const result = await compileModule(rewriteAssetUrls(`${prelude}---\n${JSON.stringify({
1544
+ const rewritten = rewriteAssetUrls(`${prelude}---\n${JSON.stringify({
1554
1545
  ...parsed.frontmatter,
1555
1546
  ...slide.frontmatter
1556
- })}\n---\n${slide.content}`, assets), { sourcePath: resolve(input.cwd ?? process.cwd(), entry.sourcePath) });
1547
+ })}\n---\n${slide.content}`, assets);
1548
+ const result = await adapter.compile(rewritten, { sourcePath: resolve(input.cwd ?? process.cwd(), entry.sourcePath) });
1557
1549
  clientEntryRequired ||= Boolean(result.clientCode || result.islands?.length);
1558
1550
  if (result.clientCode) clients.add(result.clientCode);
1559
1551
  for (const island of result.islands ?? []) markdownIslands.set(island.id, island);
@@ -1603,6 +1595,7 @@ async function compileVoidMarkdownDecks(input) {
1603
1595
  decks,
1604
1596
  routerModule: emitModuleDecksRouter({ decks }),
1605
1597
  clientCode: [...clients].join("\n"),
1598
+ clientModules: [adapter.client([...markdownIslands.values()])].filter(Boolean),
1606
1599
  markdownIslands: [...markdownIslands.values()]
1607
1600
  };
1608
1601
  }
@@ -1639,6 +1632,64 @@ function splitMarkdownSlides(source) {
1639
1632
  return splitSlideSources(protectedLines.join("\n")).map((slide) => slide.replace(new RegExp(`${prefix}(\\d+)_END`, "g"), (_, id) => fences[Number(id)]));
1640
1633
  }
1641
1634
  //#endregion
1635
+ //#region src/adapters/hono-markdown.ts
1636
+ /** Node-only bridge. Existing Void configs retain their original compiler/runtime pair. */
1637
+ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1638
+ const require = createRequire(resolve(cwd, "package.json"));
1639
+ const entry = legacy ? "void-plugin-hono-jsx/markdown/compiler" : "@ts-76/hono-markdown/compiler";
1640
+ let compiler;
1641
+ try {
1642
+ compiler = await import(pathToFileURL(require.resolve(entry)).href);
1643
+ } catch (cause) {
1644
+ throw new Error(`Hono Markdown requires ${legacy ? "its public /markdown/compiler API" : "@ts-76/hono-markdown/compiler"} and Markdown peers installed in the app.`, { cause });
1645
+ }
1646
+ const read = compiler.readMarkdownModule ?? compiler.readStaticMarkdown;
1647
+ const compile = compiler.compileMarkdownModule ?? compiler.compileStaticMarkdown;
1648
+ if (typeof read !== "function" || typeof compile !== "function") throw new Error("Missing standalone compiler API");
1649
+ const runtime = entry.startsWith("void-plugin") ? "void-plugin-hono-jsx/markdown/client" : "@ts-76/hono-markdown/client";
1650
+ return {
1651
+ read,
1652
+ compile: (source, input) => compile(source, {
1653
+ ...input,
1654
+ options
1655
+ }),
1656
+ client: (islands) => emitMarkdownClient(islands, runtime)
1657
+ };
1658
+ }
1659
+ function emitMarkdownClient(islands, runtime) {
1660
+ if (!islands.length) return "";
1661
+ const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1662
+ return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1663
+ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1664
+ ready(element, start) {
1665
+ const slide = element.closest('section[data-slide-state]');
1666
+ if (!slide || slide.dataset.slideState === 'active') { start(); return; }
1667
+ const observer = new MutationObserver(() => {
1668
+ if (slide.dataset.slideState === 'active') { observer.disconnect(); start(); }
1669
+ });
1670
+ observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1671
+ }
1672
+ });`;
1673
+ }
1674
+ //#endregion
1675
+ //#region src/node/markdown-adapter.ts
1676
+ async function loadMarkdownAdapter(selection, cwd) {
1677
+ if (selection === "void") return createHonoMarkdownAdapter(cwd, true);
1678
+ if (selection.adapter === "hono-decks/adapters/hono-markdown") return createHonoMarkdownAdapter(cwd, false, selection.options);
1679
+ const module = await import(pathToFileURL(createRequire(resolve(cwd, "package.json")).resolve(selection.adapter)).href);
1680
+ if (typeof module.createMarkdownAdapter !== "function") throw new Error("Markdown adapter must export createMarkdownAdapter.");
1681
+ const adapter = await module.createMarkdownAdapter({
1682
+ cwd,
1683
+ options: selection.options
1684
+ });
1685
+ if (![
1686
+ adapter?.read,
1687
+ adapter?.compile,
1688
+ adapter?.client
1689
+ ].every((value) => typeof value === "function")) throw new Error("Markdown adapter must provide read, compile and client functions.");
1690
+ return adapter;
1691
+ }
1692
+ //#endregion
1642
1693
  //#region src/node/client-entry.ts
1643
1694
  async function discoverClientComponentIds(input) {
1644
1695
  const result = {};
@@ -1659,12 +1710,11 @@ async function emitClientEntryModule(input) {
1659
1710
  registrations.push(`${JSON.stringify(clientId)}: ${localName}`);
1660
1711
  }
1661
1712
  }
1662
- const markdown = input.markdownIslands ?? [];
1663
- if (registrations.length === 0 && markdown.length === 0) return `export const decksClientEntry = ${JSON.stringify(input.extraCode ?? "")};\n`;
1713
+ const clientModules = input.clientModules ?? [];
1714
+ if (registrations.length === 0 && clientModules.length === 0) return `export const decksClientEntry = ${JSON.stringify(input.extraCode ?? "")};\n`;
1664
1715
  const result = await build({
1665
1716
  stdin: {
1666
- contents: `${markdown.length ? `import { hydrateMarkdownIslands } from "void-plugin-hono-jsx/markdown/client";
1667
- hydrateMarkdownIslands({ ${markdown.map((entry) => `${javascriptString(entry.id)}: () => import(${javascriptString(entry.modulePath)})`).join(",")} });` : ""}
1717
+ contents: `${clientModules.join("\n")}
1668
1718
  import { hydrateSlideIslands } from "hono-decks/client";
1669
1719
  ${imports.join("\n")}
1670
1720
 
@@ -1741,9 +1791,6 @@ function hashString(value) {
1741
1791
  function safeIdentifier(value) {
1742
1792
  return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1743
1793
  }
1744
- function javascriptString(value) {
1745
- return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1746
- }
1747
1794
  //#endregion
1748
1795
  //#region src/node/local-deck-io.ts
1749
1796
  async function listFiles(cwd, dir) {
@@ -1951,7 +1998,7 @@ async function compileDeckModules(input) {
1951
1998
  const out = normalizeRelativePath(input.out, "Output directory");
1952
1999
  const root = normalizeDeckRoot(input.root);
1953
2000
  const paths = await listFiles(input.cwd, join(input.cwd, root));
1954
- const resolved = resolveDeckFiles(paths, root, { markdown: input.markdown === "void" });
2001
+ const resolved = resolveDeckFiles(paths, root, { markdown: Boolean(input.markdown) });
1955
2002
  const componentModulePaths = Object.fromEntries(resolved.flatMap((deck) => {
1956
2003
  const path = deck.kind === "directory" ? findComponentModule(paths, root, deck.slug) : void 0;
1957
2004
  return path ? [[deck.slug, path]] : [];
@@ -1979,7 +2026,8 @@ async function compileDeckModules(input) {
1979
2026
  refresh: input.refreshOgp,
1980
2027
  resolveOgp: input.resolveOgp ?? resolveOgpMetadata
1981
2028
  });
1982
- const generated = await (input.markdown === "void" ? compileVoidMarkdownDecks : compileMdxModuleDecks)({
2029
+ const adapter = input.markdown && resolved.some((entry) => entry.sourcePath.endsWith(".md")) ? await loadMarkdownAdapter(input.markdown, input.cwd) : void 0;
2030
+ const generated = await (adapter ? (value) => compileMarkdownDecks(value, adapter) : compileMdxModuleDecks)({
1983
2031
  cwd: input.cwd,
1984
2032
  root,
1985
2033
  outDir: out,
@@ -1998,7 +2046,7 @@ async function compileDeckModules(input) {
1998
2046
  clientEntries: clientEntryPaths,
1999
2047
  clientComponentIds,
2000
2048
  extraCode: generated.clientCode,
2001
- markdownIslands: generated.markdownIslands,
2049
+ clientModules: generated.clientModules,
2002
2050
  onCss: (css) => {
2003
2051
  clientCss = css;
2004
2052
  }
@@ -2148,7 +2196,7 @@ async function loadDecksConfig(input) {
2148
2196
  const config = (await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}#${Date.now()}`)).default;
2149
2197
  if (!config || typeof config !== "object") throw new Error(`Config must have a default export: ${path}`);
2150
2198
  if (typeof config.mountPath !== "string") throw new Error(`Config mountPath must be a string: ${path}`);
2151
- if (config.build?.markdown !== void 0 && config.build.markdown !== "void") throw new Error("Config build.markdown must be \"void\".");
2199
+ if (config.build?.markdown !== void 0 && config.build.markdown !== "void" && (typeof config.build.markdown !== "object" || !config.build.markdown || typeof config.build.markdown.adapter !== "string" || !config.build.markdown.adapter.trim())) throw new Error("Config build.markdown must be \"void\" or an adapter reference.");
2152
2200
  return {
2153
2201
  path,
2154
2202
  config,
@@ -2301,7 +2349,7 @@ async function runInitCommand(input, args, stdout, stderr) {
2301
2349
  return { exitCode: 1 };
2302
2350
  }
2303
2351
  if (parsed.options.framework) try {
2304
- const { initializeVite } = await import("./init-vite-mLg3XlcN.js");
2352
+ const { initializeVite } = await import("./init-vite-EA_L4k2H.js");
2305
2353
  const sampleSlug = await initializeVite(input.cwd, parsed.options.framework, parsed.options.configFile, parsed.options.entry, parsed.options.install ? { adapter: parsed.options.adapter } : void 0);
2306
2354
  stdout(`Initialized ${parsed.options.framework} decks at /slides/${sampleSlug}. Run your usual dev or build command; no manual compile is needed.`);
2307
2355
  return { exitCode: 0 };