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