hono-decks 1.0.4 → 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/dist/bin.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { i as normalizeRelativePath, n as normalizeDeckRoot, r as normalizePath$4, t as publishGeneratedFiles } from "./file-transaction-NjDxX6Gt.js";
3
+ import { createRequire } from "node:module";
2
4
  import { existsSync, watch } from "node:fs";
3
5
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
6
  import { join, relative, resolve } from "node:path";
@@ -9,28 +11,28 @@ import remarkParse from "remark-parse";
9
11
  import { compile } from "@mdx-js/mdx";
10
12
  import remarkDirective from "remark-directive";
11
13
  import { codeToHtml } from "shiki";
12
- import { fileURLToPath } from "node:url";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
15
  import { build } from "esbuild";
14
16
  import { lookup } from "node:dns/promises";
15
17
  import { isIP } from "node:net";
16
18
  //#region src/routing/file-routing.ts
17
- function resolveDeckFiles(paths, root = "decks") {
18
- const normalizedRoot = normalizePath$4(root).replace(/\/$/, "");
19
+ function resolveDeckFiles(paths, root = "decks", options = {}) {
20
+ const normalizedRoot = normalizePath$3(root).replace(/\/$/, "");
19
21
  const decks = /* @__PURE__ */ new Map();
20
22
  const assetsBySlug = /* @__PURE__ */ new Map();
21
- for (const inputPath of paths.map(normalizePath$4).sort()) {
23
+ for (const inputPath of paths.map(normalizePath$3).sort()) {
22
24
  assertInsideRoot(inputPath, normalizedRoot);
23
25
  const segments = inputPath.slice(normalizedRoot.length + 1).split("/");
24
- if (segments.length === 1 && segments[0].endsWith(".mdx")) {
26
+ if (segments.length === 1 && (segments[0].endsWith(".mdx") || options.markdown && segments[0].endsWith(".md"))) {
25
27
  addDeck(decks, {
26
- slug: segments[0].replace(/\.mdx$/, ""),
28
+ slug: segments[0].replace(/\.mdx?$/, ""),
27
29
  sourcePath: inputPath,
28
30
  kind: "single-file",
29
31
  assetPaths: []
30
32
  });
31
33
  continue;
32
34
  }
33
- if (segments.length === 2 && segments[1] === "deck.mdx") {
35
+ if (segments.length === 2 && (segments[1] === "deck.mdx" || options.markdown && segments[1] === "deck.md")) {
34
36
  addDeck(decks, {
35
37
  slug: segments[0],
36
38
  sourcePath: inputPath,
@@ -48,7 +50,7 @@ function resolveDeckFiles(paths, root = "decks") {
48
50
  if (deck?.kind === "directory") deck.assetPaths = assetPaths;
49
51
  continue;
50
52
  }
51
- if (inputPath.endsWith(".mdx")) throw new Error(`Nested deck slugs are not supported in this slice: ${inputPath}`);
53
+ if (inputPath.endsWith(".mdx") || options.markdown && inputPath.endsWith(".md")) throw new Error(`Nested deck slugs are not supported in this slice: ${inputPath}`);
52
54
  }
53
55
  return [...decks.values()].map((deck) => ({
54
56
  ...deck,
@@ -64,7 +66,7 @@ function assertInsideRoot(path, root) {
64
66
  if (path.includes("/../") || path.endsWith("/..") || path.startsWith("../")) throw new Error(`Deck path escapes the root: ${path}`);
65
67
  if (path !== root && !path.startsWith(`${root}/`)) throw new Error(`Deck path is outside ${root}: ${path}`);
66
68
  }
67
- function normalizePath$4(path) {
69
+ function normalizePath$3(path) {
68
70
  return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
69
71
  }
70
72
  //#endregion
@@ -370,6 +372,11 @@ function collectFrontmatterAssetCandidates(value) {
370
372
  if (typeof value === "string") return [value.trim()].filter(Boolean);
371
373
  return [];
372
374
  }
375
+ function isLocalRelativeAssetCandidate(value) {
376
+ if (assetRefType(value)) return false;
377
+ if (value.startsWith("./") || value.startsWith("../")) return true;
378
+ return /^[^/:?#]+\.(?:png|jpe?g|gif|svg|webp)(?:[?#].*)?$/i.test(value);
379
+ }
373
380
  function contentTypeForPath(path) {
374
381
  const queryIndex = path.indexOf("?");
375
382
  const pathname = (queryIndex === -1 ? path : path.slice(0, queryIndex)).toLowerCase();
@@ -917,7 +924,7 @@ function rewriteAssetUrls(source, assets) {
917
924
  }
918
925
  function rewriteRelativeMdxImports(source, sourceDir, generatedDir) {
919
926
  return source.replace(/(from\s+|import\s+)(["'])(\.[^"']+)\2/g, (_match, prefix, quote, specifier) => {
920
- return `${prefix}${quote}${toRelativeImportPath(generatedDir, normalizePath$3(`${sourceDir}/${specifier}`))}${quote}`;
927
+ return `${prefix}${quote}${toRelativeImportPath(generatedDir, normalizePath$2(`${sourceDir}/${specifier}`))}${quote}`;
921
928
  });
922
929
  }
923
930
  function componentImportPath(outDir, sourcePath) {
@@ -925,22 +932,22 @@ function componentImportPath(outDir, sourcePath) {
925
932
  return toRelativeImportPath(outDir, sourcePath.replace(/\/index\.(tsx|ts|jsx|js)$/, ""));
926
933
  }
927
934
  function dirname$2(path) {
928
- const normalized = normalizePath$3(path);
935
+ const normalized = normalizePath$2(path);
929
936
  const index = normalized.lastIndexOf("/");
930
937
  return index === -1 ? "." : normalized.slice(0, index);
931
938
  }
932
939
  function localAssetRelativePath(sourcePath) {
933
940
  const marker = "/assets/";
934
- const normalized = normalizePath$3(sourcePath);
941
+ const normalized = normalizePath$2(sourcePath);
935
942
  const markerIndex = normalized.indexOf(marker);
936
943
  return markerIndex === -1 ? normalized.split("/").at(-1) ?? normalized : normalized.slice(markerIndex + 8);
937
944
  }
938
- function normalizePath$3(path) {
945
+ function normalizePath$2(path) {
939
946
  return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
940
947
  }
941
948
  function toRelativeImportPath(fromDir, target) {
942
- const fromParts = normalizePath$3(fromDir).split("/").filter(Boolean);
943
- const targetParts = normalizePath$3(target).split("/").filter(Boolean);
949
+ const fromParts = normalizePath$2(fromDir).split("/").filter(Boolean);
950
+ const targetParts = normalizePath$2(target).split("/").filter(Boolean);
944
951
  while (fromParts.length > 0 && targetParts.length > 0 && fromParts[0] === targetParts[0]) {
945
952
  fromParts.shift();
946
953
  targetParts.shift();
@@ -963,14 +970,14 @@ async function buildAssetRefs(slug, assetPaths, input) {
963
970
  })));
964
971
  }
965
972
  function assetName(sourcePath, root, slug) {
966
- const normalizedPath = normalizePath$2(sourcePath);
967
- const prefix = `${normalizePath$2(root).replace(/\/$/, "")}/${slug}/assets/`;
973
+ const normalizedPath = normalizePath$1(sourcePath);
974
+ const prefix = `${normalizePath$1(root).replace(/\/$/, "")}/${slug}/assets/`;
968
975
  return (normalizedPath.startsWith(prefix) ? normalizedPath.slice(prefix.length) : normalizedPath.split("/").at(-1) ?? normalizedPath).split("/").map(encodeURIComponent).join("/");
969
976
  }
970
977
  function normalizeMountPath(value) {
971
978
  return (value.startsWith("/") ? value : `/${value}`).replace(/\/$/, "");
972
979
  }
973
- function normalizePath$2(path) {
980
+ function normalizePath$1(path) {
974
981
  return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
975
982
  }
976
983
  //#endregion
@@ -986,8 +993,8 @@ import { configureDecks, defineDecks } from ${JSON.stringify(DECKS_ADVANCED_ENTR
986
993
  import type { ConfiguredDecks, DecksConfig } from ${runtimeEntry};
987
994
  import type { Env } from "hono";
988
995
  import { decksClientEntry } from "./client-entry";
989
- ${input.decks.flatMap((deck) => deck.slideModules.map((slide, index) => `import ${slideImportName(deck.deck.slug, index)} from ${JSON.stringify(slide.importPath)};`)).join("\n")}
990
- ${input.decks.filter((deck) => deck.componentModulePath).map((deck) => `import * as ${componentImportName(deck.deck.slug)} from ${JSON.stringify(deck.componentModulePath)};`).join("\n")}
996
+ ${input.decks.flatMap((deck, deckIndex) => deck.slideModules.map((slide, index) => `import ${slideImportName(deckIndex, index)} from ${JSON.stringify(slide.importPath)};`)).join("\n")}
997
+ ${input.decks.flatMap((deck, deckIndex) => deck.componentModulePath ? [`import * as ${componentImportName(deckIndex)} from ${JSON.stringify(deck.componentModulePath)};`] : []).join("\n")}
991
998
 
992
999
  function withClientComponentIds(module, clientIds) {
993
1000
  const registry = {};
@@ -1007,7 +1014,7 @@ function withClientComponentIds(module, clientIds) {
1007
1014
  const generatedDecks = defineDecks({
1008
1015
  clientEntryAsset: decksClientEntry,
1009
1016
  decks: [
1010
- ${input.decks.map(emitDeckObject).join(",\n")}
1017
+ ${input.decks.map((deck, deckIndex) => emitDeckObject(deck, deckIndex)).join(",\n")}
1011
1018
  ]
1012
1019
  });
1013
1020
 
@@ -1020,15 +1027,16 @@ function definedDecksFor<E extends Env>() {
1020
1027
  }
1021
1028
  `;
1022
1029
  }
1023
- function emitDeckObject(deck) {
1030
+ function emitDeckObject(deck, deckIndex) {
1024
1031
  return ` {
1025
1032
  slug: ${JSON.stringify(deck.deck.slug)},
1026
1033
  sourcePath: ${JSON.stringify(deck.deck.sourcePath)},
1027
1034
  kind: ${JSON.stringify(deck.deck.kind)},
1028
1035
  meta: ${serializeValue(deck.deck.meta, 3)},
1036
+ ${typeof deck.deck.clientEntryRequired === "boolean" ? ` clientEntryRequired: ${deck.deck.clientEntryRequired},\n` : ""}
1029
1037
  ${deck.deck.themeStyle ? ` "themeStyle": ${serializeValue(deck.deck.themeStyle, 3)},\n` : ""}${deck.deck.themeSourcePath ? ` "themeSourcePath": ${JSON.stringify(deck.deck.themeSourcePath)},\n` : ""}
1030
1038
  assets: ${serializeValue(deck.deck.assets, 3)},
1031
- componentRegistry: ${deck.componentModulePath ? `withClientComponentIds(${componentImportName(deck.deck.slug)}, ${serializeValue(deck.clientComponentIds ?? {}, 3)})` : "{}"},
1039
+ componentRegistry: ${deck.componentModulePath ? `withClientComponentIds(${componentImportName(deckIndex)}, ${serializeValue(deck.clientComponentIds ?? {}, 3)})` : "{}"},
1032
1040
  warnings: ${serializeValue(deck.deck.warnings, 3)},
1033
1041
  slides: [
1034
1042
  ${deck.deck.slides.map((slide) => ` {
@@ -1037,19 +1045,16 @@ ${deck.deck.slides.map((slide) => ` {
1037
1045
  html: "",
1038
1046
  components: [],
1039
1047
  notes: ${serializeValue(slide.notes, 5)},
1040
- render: ${slideImportName(deck.deck.slug, slide.index)}
1048
+ render: ${slideImportName(deckIndex, slide.index)}
1041
1049
  }`).join(",\n")}
1042
1050
  ]
1043
1051
  }`;
1044
1052
  }
1045
- function slideImportName(slug, index) {
1046
- return `Slide_${safeIdentifier$1(slug)}_${index}`;
1053
+ function slideImportName(deckIndex, slideIndex) {
1054
+ return `Slide_${deckIndex}_${slideIndex}`;
1047
1055
  }
1048
- function componentImportName(slug) {
1049
- return `Components_${safeIdentifier$1(slug)}`;
1050
- }
1051
- function safeIdentifier$1(value) {
1052
- return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1056
+ function componentImportName(deckIndex) {
1057
+ return `Components_${deckIndex}`;
1053
1058
  }
1054
1059
  function serializeValue(value, depth) {
1055
1060
  const indent = " ".repeat(depth);
@@ -1501,6 +1506,191 @@ function extractDeckPrelude(source) {
1501
1506
  };
1502
1507
  }
1503
1508
  //#endregion
1509
+ //#region src/node/markdown-decks.ts
1510
+ async function compileMarkdownDecks(input, adapter) {
1511
+ const markdownEntries = input.decks.filter((entry) => entry.sourcePath.endsWith(".md"));
1512
+ const mdx = await compileMdxModuleDecks({
1513
+ ...input,
1514
+ decks: input.decks.filter((entry) => !entry.sourcePath.endsWith(".md"))
1515
+ });
1516
+ if (!markdownEntries.length) return mdx;
1517
+ const decks = [...mdx.decks];
1518
+ const clients = /* @__PURE__ */ new Set();
1519
+ const markdownIslands = /* @__PURE__ */ new Map();
1520
+ for (const entry of markdownEntries) try {
1521
+ const source = await input.readText(entry.sourcePath);
1522
+ const parsed = adapter.read(source);
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.");
1524
+ const warnings = [];
1525
+ const meta = toDeckFrontmatter(parsed.frontmatter, warnings);
1526
+ addUnknownFrontmatterWarnings(warnings, meta.meta, "deck");
1527
+ const assets = [...await buildAssetRefs(entry.slug, entry.assetPaths, input), ...buildExternalAssetRefs([
1528
+ ...collectMarkdownAssetCandidates(parsed.content),
1529
+ ...collectFrontmatterAssetCandidates(meta.assets),
1530
+ ...collectFrontmatterAssetCandidates(parsed.frontmatter.background)
1531
+ ])];
1532
+ const candidates = [
1533
+ ...collectMarkdownAssetCandidates(parsed.content),
1534
+ ...collectFrontmatterAssetCandidates(parsed.frontmatter.assets),
1535
+ ...collectFrontmatterAssetCandidates(parsed.frontmatter.background)
1536
+ ];
1537
+ if (entry.kind === "single-file" && candidates.some(isLocalRelativeAssetCandidate)) throw new Error("Single-file Markdown decks cannot reference local relative assets; use a directory deck.");
1538
+ let clientEntryRequired = false;
1539
+ const slideModules = [];
1540
+ const slides = [];
1541
+ for (const [index, slideSource] of splitMarkdownSlides(parsed.content).entries()) {
1542
+ const prelude = parsed.script ? `<script>\n${parsed.script}\n<\/script>\n` : "";
1543
+ const slide = adapter.read(prelude + slideSource);
1544
+ const path = `${input.outDir}/decks/${entry.slug}/slide-${index}.ts`;
1545
+ const rewritten = rewriteAssetUrls(`${prelude}---\n${JSON.stringify({
1546
+ ...parsed.frontmatter,
1547
+ ...slide.frontmatter
1548
+ })}\n---\n${slide.content}`, assets);
1549
+ const result = await adapter.compile(rewritten, { sourcePath: resolve(input.cwd ?? process.cwd(), entry.sourcePath) });
1550
+ clientEntryRequired ||= Boolean(result.clientCode || result.islands?.length);
1551
+ if (result.clientCode) clients.add(result.clientCode);
1552
+ for (const island of result.islands ?? []) markdownIslands.set(island.id, island);
1553
+ const slideMeta = toSlideFrontmatter(slide.frontmatter, warnings, {
1554
+ slideIndex: index,
1555
+ fallbackTitle: result.headings[0]?.text,
1556
+ fallbackTransition: meta.transition,
1557
+ fallbackTransitionDuration: meta.transitionDuration,
1558
+ fallbackTransitionEasing: meta.transitionEasing
1559
+ });
1560
+ addUnknownFrontmatterWarnings(warnings, slideMeta.meta, "slide", index);
1561
+ slideModules.push({
1562
+ path,
1563
+ importPath: `./decks/${entry.slug}/slide-${index}`,
1564
+ code: `// @ts-nocheck\n${rewriteRelativeMdxImports(result.code, dirname$2(entry.sourcePath), dirname$2(path))}`
1565
+ });
1566
+ slides.push({
1567
+ index,
1568
+ meta: slideMeta,
1569
+ html: "",
1570
+ components: [],
1571
+ notes: slideMeta.notes
1572
+ });
1573
+ }
1574
+ addExternalAssetWarnings(warnings, assets);
1575
+ const themeStyle = input.themeStyles?.[entry.slug];
1576
+ decks.push({
1577
+ deck: {
1578
+ slug: entry.slug,
1579
+ sourcePath: entry.sourcePath,
1580
+ kind: entry.kind,
1581
+ clientEntryRequired,
1582
+ meta,
1583
+ slides,
1584
+ assets,
1585
+ warnings,
1586
+ themeStyle: themeStyle?.style,
1587
+ themeSourcePath: themeStyle?.sourcePath
1588
+ },
1589
+ slideModules
1590
+ });
1591
+ } catch (error) {
1592
+ throw new CompileError(`Void Markdown compile failed in ${entry.sourcePath}: ${error instanceof Error ? error.message : String(error)}`, "void-markdown-compile-error");
1593
+ }
1594
+ decks.sort((a, b) => a.deck.slug.localeCompare(b.deck.slug));
1595
+ return {
1596
+ decks,
1597
+ routerModule: emitModuleDecksRouter({ decks }),
1598
+ clientCode: [...clients].join("\n"),
1599
+ clientModules: [adapter.client([...markdownIslands.values()])].filter(Boolean),
1600
+ markdownIslands: [...markdownIslands.values()]
1601
+ };
1602
+ }
1603
+ /** Protect fenced code from the deck separator parser. */
1604
+ function splitMarkdownSlides(source) {
1605
+ const fences = [];
1606
+ let prefix = "HONO_DECKS_FENCE_";
1607
+ while (source.includes(prefix)) prefix += "_";
1608
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
1609
+ const protectedLines = [];
1610
+ for (let index = 0; index < lines.length; index++) {
1611
+ const open = /^ {0,3}(`{3,}|~{3,})/.exec(lines[index]);
1612
+ if (/^[ \t]+---[ \t]*$/.test(lines[index])) {
1613
+ protectedLines.push(`${prefix}${fences.push(lines[index]) - 1}_END`);
1614
+ continue;
1615
+ }
1616
+ if (!open) {
1617
+ protectedLines.push(lines[index]);
1618
+ continue;
1619
+ }
1620
+ const block = [lines[index]];
1621
+ const close = new RegExp(`^ {0,3}${open[1][0]}{${open[1].length},}[ \\t]*$`);
1622
+ let closed = false;
1623
+ while (++index < lines.length) {
1624
+ block.push(lines[index]);
1625
+ if (close.test(lines[index])) {
1626
+ closed = true;
1627
+ break;
1628
+ }
1629
+ }
1630
+ if (!closed) throw new Error("Markdown code fence is not closed.");
1631
+ protectedLines.push(`${prefix}${fences.push(block.join("\n")) - 1}_END`);
1632
+ }
1633
+ return splitSlideSources(protectedLines.join("\n")).map((slide) => slide.replace(new RegExp(`${prefix}(\\d+)_END`, "g"), (_, id) => fences[Number(id)]));
1634
+ }
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
1504
1694
  //#region src/node/client-entry.ts
1505
1695
  async function discoverClientComponentIds(input) {
1506
1696
  const result = {};
@@ -1521,10 +1711,12 @@ async function emitClientEntryModule(input) {
1521
1711
  registrations.push(`${JSON.stringify(clientId)}: ${localName}`);
1522
1712
  }
1523
1713
  }
1524
- if (registrations.length === 0) return "export const decksClientEntry = \"\";\n";
1525
- const output = (await build({
1714
+ const clientModules = input.clientModules ?? [];
1715
+ if (registrations.length === 0 && clientModules.length === 0) return `export const decksClientEntry = ${JSON.stringify(input.extraCode ?? "")};\n`;
1716
+ const result = await build({
1526
1717
  stdin: {
1527
- contents: `import { hydrateSlideIslands } from "hono-decks/client";
1718
+ contents: `${clientModules.join("\n")}
1719
+ import { hydrateSlideIslands } from "hono-decks/client";
1528
1720
  ${imports.join("\n")}
1529
1721
 
1530
1722
  hydrateSlideIslands({
@@ -1539,6 +1731,15 @@ hydrateSlideIslands({
1539
1731
  },
1540
1732
  bundle: true,
1541
1733
  write: false,
1734
+ outdir: join(input.cwd, ".hono-decks-client"),
1735
+ loader: {
1736
+ ".svg": "dataurl",
1737
+ ".png": "dataurl",
1738
+ ".jpg": "dataurl",
1739
+ ".webp": "dataurl",
1740
+ ".woff2": "dataurl",
1741
+ ".woff": "dataurl"
1742
+ },
1542
1743
  format: "esm",
1543
1744
  platform: "browser",
1544
1745
  target: "es2022",
@@ -1548,9 +1749,12 @@ hydrateSlideIslands({
1548
1749
  alias: { "hono-decks/client": resolveClientRuntimeEntry() },
1549
1750
  sourcemap: false,
1550
1751
  minify: false
1551
- })).outputFiles[0];
1752
+ });
1753
+ const output = result.outputFiles.find((file) => file.path.endsWith(".js"));
1754
+ const css = result.outputFiles.filter((file) => file.path.endsWith(".css")).map((file) => file.text).join("\n");
1755
+ input.onCss?.(css);
1552
1756
  if (!output) throw new Error("Client entry did not produce output.");
1553
- return `export const decksClientEntry = ${JSON.stringify(output.text)};\n`;
1757
+ return `export const decksClientEntry = ${JSON.stringify(output.text + (input.extraCode ? "\n" + input.extraCode : ""))};\n`;
1554
1758
  }
1555
1759
  function resolveClientRuntimeEntry() {
1556
1760
  const built = fileURLToPath(new URL("./client.js", import.meta.url));
@@ -1589,33 +1793,13 @@ function safeIdentifier(value) {
1589
1793
  return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1590
1794
  }
1591
1795
  //#endregion
1592
- //#region src/node/path-utils.ts
1593
- function dirname$1(path) {
1594
- const normalized = normalizePath$1(path);
1595
- return normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
1596
- }
1597
- function normalizePath$1(path) {
1598
- return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
1599
- }
1600
- function normalizeDeckRoot(root) {
1601
- const normalized = normalizeRelativePath(root, "Deck root").replace(/\/$/, "");
1602
- if (normalized === ".") throw new Error("Deck root must be a relative path inside the current working directory");
1603
- return normalized;
1604
- }
1605
- function normalizeRelativePath(path, label) {
1606
- const normalized = normalizePath$1(path).replace(/\/$/, "");
1607
- const segments = normalized.split("/");
1608
- if (normalized === "" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.includes("..")) throw new Error(`${label} must be a relative path inside the current working directory`);
1609
- return normalized;
1610
- }
1611
- //#endregion
1612
1796
  //#region src/node/local-deck-io.ts
1613
1797
  async function listFiles(cwd, dir) {
1614
1798
  const entries = await readdir(dir, { withFileTypes: true });
1615
1799
  return (await Promise.all(entries.map(async (entry) => {
1616
1800
  const fullPath = join(dir, entry.name);
1617
1801
  if (entry.isDirectory()) return listFiles(cwd, fullPath);
1618
- if (entry.isFile()) return [normalizePath$1(relative(cwd, fullPath))];
1802
+ if (entry.isFile()) return [normalizePath$4(relative(cwd, fullPath))];
1619
1803
  return [];
1620
1804
  }))).flat().sort();
1621
1805
  }
@@ -1806,10 +1990,16 @@ function isHttpUrl(value) {
1806
1990
  //#endregion
1807
1991
  //#region src/node/compile-decks.ts
1808
1992
  async function compileDecks(input) {
1993
+ const compiled = await compileDeckModules(input);
1994
+ await publishGeneratedFiles([...compiled.modules, ...compiled.cacheFile ? [compiled.cacheFile] : []]);
1995
+ return compiled.result;
1996
+ }
1997
+ /** Shared by file output and Vite's in-memory module graph. Does not write files. */
1998
+ async function compileDeckModules(input) {
1809
1999
  const out = normalizeRelativePath(input.out, "Output directory");
1810
2000
  const root = normalizeDeckRoot(input.root);
1811
2001
  const paths = await listFiles(input.cwd, join(input.cwd, root));
1812
- const resolved = resolveDeckFiles(paths, root);
2002
+ const resolved = resolveDeckFiles(paths, root, { markdown: Boolean(input.markdown) });
1813
2003
  const componentModulePaths = Object.fromEntries(resolved.flatMap((deck) => {
1814
2004
  const path = deck.kind === "directory" ? findComponentModule(paths, root, deck.slug) : void 0;
1815
2005
  return path ? [[deck.slug, path]] : [];
@@ -1837,7 +2027,9 @@ async function compileDecks(input) {
1837
2027
  refresh: input.refreshOgp,
1838
2028
  resolveOgp: input.resolveOgp ?? resolveOgpMetadata
1839
2029
  });
1840
- const generated = await 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)({
2032
+ cwd: input.cwd,
1841
2033
  root,
1842
2034
  outDir: out,
1843
2035
  mountPath: input.mountPath,
@@ -1849,20 +2041,57 @@ async function compileDecks(input) {
1849
2041
  readText: (path) => readFile(join(input.cwd, path), "utf8"),
1850
2042
  readBinary: (path) => readFile(join(input.cwd, path))
1851
2043
  });
1852
- for (const deck of generated.decks) for (const slide of deck.slideModules) await writeTextFile(join(input.cwd, slide.path), slide.code);
1853
- await writeTextFile(join(input.cwd, out, "decks.ts"), generated.routerModule);
1854
- await writeTextFile(join(input.cwd, out, "client-entry.ts"), await emitClientEntryModule({
2044
+ let clientCss = "";
2045
+ const clientEntry = await emitClientEntryModule({
1855
2046
  cwd: input.cwd,
1856
2047
  clientEntries: clientEntryPaths,
1857
- clientComponentIds
1858
- }));
1859
- await ogpCache.write();
1860
- return { decks: generated.decks.map((deck) => deck.deck) };
2048
+ clientComponentIds,
2049
+ extraCode: generated.clientCode,
2050
+ clientModules: generated.clientModules,
2051
+ onCss: (css) => {
2052
+ clientCss = css;
2053
+ }
2054
+ });
2055
+ if (clientCss) {
2056
+ for (const deck of generated.decks) if (deck.deck.clientEntryRequired || Object.keys(deck.clientComponentIds ?? {}).length) deck.deck.themeStyle = [deck.deck.themeStyle, clientCss].filter(Boolean).join("\n");
2057
+ generated.routerModule = emitModuleDecksRouter({ decks: generated.decks });
2058
+ }
2059
+ const files = generated.decks.flatMap((deck) => deck.slideModules.map((slide) => ({
2060
+ path: join(input.cwd, slide.path),
2061
+ content: slide.code
2062
+ })));
2063
+ files.push({
2064
+ path: join(input.cwd, out, "decks.ts"),
2065
+ content: generated.routerModule
2066
+ }, {
2067
+ path: join(input.cwd, out, "client-entry.ts"),
2068
+ content: clientEntry
2069
+ });
2070
+ return {
2071
+ modules: files,
2072
+ cacheFile: ogpCache.output(),
2073
+ result: {
2074
+ outDir: out,
2075
+ routerModulePath: `${out}/decks.ts`,
2076
+ decks: generated.decks.map(({ deck, slideModules }) => {
2077
+ const { slides, componentRegistry: _componentRegistry, ...metadata } = deck;
2078
+ return {
2079
+ ...metadata,
2080
+ slides: slides.map((slide, index) => ({
2081
+ index: slide.index,
2082
+ meta: slide.meta,
2083
+ notes: slide.notes,
2084
+ modulePath: slideModules[index].path
2085
+ }))
2086
+ };
2087
+ })
2088
+ }
2089
+ };
1861
2090
  }
1862
2091
  async function createOgpCacheResolver(input) {
1863
2092
  if (!input.cacheFile) return {
1864
2093
  resolveOgp: input.resolveOgp,
1865
- write: async () => void 0
2094
+ output: () => void 0
1866
2095
  };
1867
2096
  const cachePath = join(input.cwd, normalizeRelativePath(input.cacheFile, "OGP cache file"));
1868
2097
  const cache = await readOgpCache(cachePath);
@@ -1878,9 +2107,11 @@ async function createOgpCacheResolver(input) {
1878
2107
  }
1879
2108
  return metadata;
1880
2109
  },
1881
- async write() {
1882
- if (!dirty) return;
1883
- await writeTextFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`);
2110
+ output() {
2111
+ return dirty ? {
2112
+ path: cachePath,
2113
+ content: `${JSON.stringify(cache, null, 2)}\n`
2114
+ } : void 0;
1884
2115
  }
1885
2116
  };
1886
2117
  }
@@ -1910,21 +2141,17 @@ async function readOgpCache(path) {
1910
2141
  function isNodeError(error) {
1911
2142
  return error instanceof Error && "code" in error;
1912
2143
  }
1913
- async function writeTextFile(path, contents) {
1914
- await mkdir(dirname$1(path), { recursive: true });
1915
- await writeFile(path, contents, "utf8");
1916
- }
1917
2144
  function findComponentModule(paths, root, slug) {
1918
- const base = `${normalizePath$1(root).replace(/\/$/, "")}/${slug}/components/index`;
2145
+ const base = `${normalizePath$4(root).replace(/\/$/, "")}/${slug}/components/index`;
1919
2146
  return paths.find((path) => {
1920
- const normalized = normalizePath$1(path);
2147
+ const normalized = normalizePath$4(path);
1921
2148
  return normalized === `${base}.tsx` || normalized === `${base}.ts` || normalized === `${base}.jsx` || normalized === `${base}.js`;
1922
2149
  });
1923
2150
  }
1924
2151
  function findClientEntryModule(paths, root, slug) {
1925
- const base = `${normalizePath$1(root).replace(/\/$/, "")}/${slug}/components/client/index`;
2152
+ const base = `${normalizePath$4(root).replace(/\/$/, "")}/${slug}/components/client/index`;
1926
2153
  return paths.find((path) => {
1927
- const normalized = normalizePath$1(path);
2154
+ const normalized = normalizePath$4(path);
1928
2155
  return normalized === `${base}.tsx` || normalized === `${base}.ts` || normalized === `${base}.jsx` || normalized === `${base}.js`;
1929
2156
  });
1930
2157
  }
@@ -1942,7 +2169,7 @@ async function discoverDeckThemeStyles(input) {
1942
2169
  return result;
1943
2170
  }
1944
2171
  function findDeckThemeStyle(paths, root, slug) {
1945
- const base = `${normalizePath$1(root).replace(/\/$/, "")}/${slug}`;
2172
+ const base = `${normalizePath$4(root).replace(/\/$/, "")}/${slug}`;
1946
2173
  const themePath = `${base}/theme.css`;
1947
2174
  const stylesEntryPath = `${base}/styles/index.css`;
1948
2175
  const hasTheme = paths.includes(themePath);
@@ -1953,7 +2180,7 @@ function findDeckThemeStyle(paths, root, slug) {
1953
2180
  async function loadDecksConfig(input) {
1954
2181
  const path = resolve(input.cwd, input.configFile ?? "hono-decks.config.ts");
1955
2182
  if (!existsSync(path)) throw new Error(`Config file not found: ${input.configFile ?? "hono-decks.config.ts"}. Run \`hono-decks init\` first.`);
1956
- const source = (await build({
2183
+ const result = await build({
1957
2184
  absWorkingDir: input.cwd,
1958
2185
  entryPoints: [path],
1959
2186
  bundle: true,
@@ -1961,16 +2188,20 @@ async function loadDecksConfig(input) {
1961
2188
  platform: "node",
1962
2189
  target: "node20",
1963
2190
  write: false,
2191
+ metafile: true,
1964
2192
  logLevel: "silent",
1965
2193
  alias: { "hono-decks": resolveRuntimeEntry() }
1966
- })).outputFiles[0]?.text;
2194
+ });
2195
+ const source = result.outputFiles[0]?.text;
1967
2196
  if (!source) throw new Error(`Could not load config file: ${path}`);
1968
2197
  const config = (await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}#${Date.now()}`)).default;
1969
2198
  if (!config || typeof config !== "object") throw new Error(`Config must have a default export: ${path}`);
1970
2199
  if (typeof config.mountPath !== "string") throw new Error(`Config mountPath must be a string: ${path}`);
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.");
1971
2201
  return {
1972
2202
  path,
1973
2203
  config,
2204
+ dependencies: Object.keys(result.metafile.inputs).map((inputPath) => resolve(input.cwd, inputPath)),
1974
2205
  root: config.build?.root ?? "decks",
1975
2206
  outDir: config.build?.outDir ?? "src/generated",
1976
2207
  ogpCacheFile: config.build?.ogpCacheFile
@@ -1992,6 +2223,10 @@ Commands:
1992
2223
  compile, build Compile decks using the shared config.
1993
2224
 
1994
2225
  Options:
2226
+ --install Install missing dependencies for --framework void.
2227
+ --adapter <specifier> Compatible Hono Markdown adapter version or tarball; requires --install.
2228
+ --framework <name> Vite integration: hono, honox, or void.
2229
+ --entry <path> Hono app entry for --framework hono; otherwise auto-detect.
1995
2230
  --config <path> Config file. Default: hono-decks.config.ts
1996
2231
  --out <path> Facade output for init. Default: src/decks.ts
1997
2232
  --generated <path> Generated module import for init. Default: ./generated/decks
@@ -2033,6 +2268,7 @@ async function runHonoDecksCli(input) {
2033
2268
  out: loaded.outDir,
2034
2269
  mountPath: loaded.config.mountPath,
2035
2270
  ogpCacheFile: loaded.ogpCacheFile,
2271
+ markdown: loaded.config.build?.markdown,
2036
2272
  refreshOgp: parsed.options.refreshOgp
2037
2273
  });
2038
2274
  stdout(`Compiled ${manifest.decks.length} decks to ${loaded.outDir}`);
@@ -2113,6 +2349,15 @@ async function runInitCommand(input, args, stdout, stderr) {
2113
2349
  stderr(USAGE);
2114
2350
  return { exitCode: 1 };
2115
2351
  }
2352
+ if (parsed.options.framework) try {
2353
+ const { initializeVite } = await import("./init-vite-EA_L4k2H.js");
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);
2355
+ stdout(`Initialized ${parsed.options.framework} decks at /slides/${sampleSlug}. Run your usual dev or build command; no manual compile is needed.`);
2356
+ return { exitCode: 0 };
2357
+ } catch (error) {
2358
+ stderr(error instanceof Error ? error.message : String(error));
2359
+ return { exitCode: 1 };
2360
+ }
2116
2361
  const configFile = normalizeOutputFile(parsed.options.configFile ?? "hono-decks.config.ts");
2117
2362
  const out = normalizeOutputFile(parsed.options.out ?? "src/decks.ts");
2118
2363
  try {
@@ -2170,13 +2415,26 @@ function parseInitArgs(args) {
2170
2415
  options,
2171
2416
  help: true
2172
2417
  };
2173
- if (arg === "--config" || arg === "--out" || arg === "--generated") {
2418
+ if (arg === "--install") {
2419
+ options.install = true;
2420
+ continue;
2421
+ }
2422
+ if (arg === "--adapter" || arg === "--config" || arg === "--out" || arg === "--generated" || arg === "--framework" || arg === "--entry") {
2174
2423
  const value = args[index + 1];
2175
2424
  if (!value || value.startsWith("--")) return {
2176
2425
  options,
2177
2426
  error: `Missing value for ${arg}`
2178
2427
  };
2428
+ if (arg === "--framework") {
2429
+ if (value !== "void" && value !== "hono" && value !== "honox") return {
2430
+ options,
2431
+ error: `Unsupported framework: ${value}. Expected hono, honox, or void.`
2432
+ };
2433
+ options.framework = value;
2434
+ }
2435
+ if (arg === "--adapter") options.adapter = value;
2179
2436
  if (arg === "--config") options.configFile = value;
2437
+ if (arg === "--entry") options.entry = value;
2180
2438
  if (arg === "--out") options.out = value;
2181
2439
  if (arg === "--generated") options.generated = value;
2182
2440
  index += 1;
@@ -2187,14 +2445,26 @@ function parseInitArgs(args) {
2187
2445
  error: `Unknown option: ${arg}`
2188
2446
  };
2189
2447
  }
2448
+ if (options.install && options.framework !== "void" || options.adapter && !options.install) return {
2449
+ options,
2450
+ error: "--install requires --framework void; --adapter also requires --install."
2451
+ };
2452
+ if (options.entry && options.framework !== "hono") return {
2453
+ options,
2454
+ error: "--entry requires --framework hono."
2455
+ };
2456
+ if (options.framework && (options.out || options.generated)) return {
2457
+ options,
2458
+ error: "Framework init uses virtual modules; --out and --generated are only supported by generic init."
2459
+ };
2190
2460
  return { options };
2191
2461
  }
2192
2462
  async function writeInitialFiles(input) {
2193
2463
  for (const path of [input.configFile, input.out]) if (existsSync(join(input.cwd, path))) throw new Error(`Refusing to overwrite existing file: ${path}`);
2194
2464
  const configPath = join(input.cwd, input.configFile);
2195
2465
  const facadePath = join(input.cwd, input.out);
2196
- await mkdir(dirname(configPath), { recursive: true });
2197
- await mkdir(dirname(facadePath), { recursive: true });
2466
+ await mkdir(dirname$1(configPath), { recursive: true });
2467
+ await mkdir(dirname$1(facadePath), { recursive: true });
2198
2468
  await writeFile(configPath, emitDecksConfig(), "utf8");
2199
2469
  await writeFile(facadePath, emitDecksFacade(input.generated, relativeImport(input.out, input.configFile)), "utf8");
2200
2470
  }
@@ -2219,7 +2489,7 @@ export const decks = createDecks(config);
2219
2489
  `;
2220
2490
  }
2221
2491
  function relativeImport(fromFile, toFile) {
2222
- const path = normalizePath(relative(dirname(fromFile), toFile)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
2492
+ const path = normalizePath(relative(dirname$1(fromFile), toFile)).replace(/\.(?:[cm]?[jt]sx?)$/, "");
2223
2493
  return path.startsWith(".") ? path : `./${path}`;
2224
2494
  }
2225
2495
  function normalizeOutputFile(path) {
@@ -2231,7 +2501,7 @@ function normalizeOutputFile(path) {
2231
2501
  function normalizePath(path) {
2232
2502
  return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
2233
2503
  }
2234
- function dirname(path) {
2504
+ function dirname$1(path) {
2235
2505
  const normalized = normalizePath(path);
2236
2506
  return normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
2237
2507
  }