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/vite.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { createRequire } from "node:module";
2
+ import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
1
3
  import { join, relative, resolve } from "node:path";
2
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import { mergeConfig } from "vite";
5
+ import { build, transform } from "esbuild";
3
6
  import { unified } from "unified";
4
7
  import remarkGfm from "remark-gfm";
5
8
  import remarkMdx from "remark-mdx";
@@ -7,29 +10,28 @@ import remarkParse from "remark-parse";
7
10
  import { compile } from "@mdx-js/mdx";
8
11
  import remarkDirective from "remark-directive";
9
12
  import { codeToHtml } from "shiki";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
14
  import { existsSync } from "node:fs";
11
- import { fileURLToPath } from "node:url";
12
- 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") {
18
+ function resolveDeckFiles(paths, root = "decks", options = {}) {
17
19
  const normalizedRoot = normalizePath$3(root).replace(/\/$/, "");
18
20
  const decks = /* @__PURE__ */ new Map();
19
21
  const assetsBySlug = /* @__PURE__ */ new Map();
20
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,
@@ -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();
@@ -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}`;
1046
- }
1047
- function componentImportName(slug) {
1048
- return `Components_${safeIdentifier$1(slug)}`;
1052
+ function slideImportName(deckIndex, slideIndex) {
1053
+ return `Slide_${deckIndex}_${slideIndex}`;
1049
1054
  }
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$1(entry.sourcePath), dirname$1(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));
@@ -1619,6 +1823,63 @@ async function listFiles(cwd, dir) {
1619
1823
  }))).flat().sort();
1620
1824
  }
1621
1825
  //#endregion
1826
+ //#region src/node/file-transaction.ts
1827
+ /** Stage every file before publishing; restore the previous files on an I/O error.
1828
+ * Individual renames are atomic. This is rollback on failure, not a cross-process
1829
+ * or crash-atomic filesystem transaction. Failed rollback retains recovery files.
1830
+ */
1831
+ async function publishGeneratedFiles(files, renameFile = rename) {
1832
+ const staged = [];
1833
+ const published = [];
1834
+ let keepRecovery = false;
1835
+ try {
1836
+ if (new Set(files.map((file) => file.path)).size !== files.length) throw new Error("Generated output paths must be unique.");
1837
+ for (const file of files) {
1838
+ const parent = dirname(file.path);
1839
+ await mkdir(parent, { recursive: true });
1840
+ const dir = await mkdtemp(join(parent, ".hono-decks-"));
1841
+ const entry = {
1842
+ path: file.path,
1843
+ dir,
1844
+ next: join(dir, "next")
1845
+ };
1846
+ staged.push(entry);
1847
+ await writeFile(entry.next, file.content, "utf8");
1848
+ let previous;
1849
+ try {
1850
+ previous = await readFile(file.path);
1851
+ } catch (error) {
1852
+ if (error.code === "ENOENT") continue;
1853
+ throw error;
1854
+ }
1855
+ entry.backup = join(dir, "previous");
1856
+ await writeFile(entry.backup, previous);
1857
+ }
1858
+ for (const entry of staged) {
1859
+ await renameFile(entry.next, entry.path);
1860
+ published.push(entry);
1861
+ }
1862
+ } catch (error) {
1863
+ const failures = [];
1864
+ for (const entry of published.reverse()) try {
1865
+ if (entry.backup) await renameFile(entry.backup, entry.path);
1866
+ else await rm(entry.path, { force: true });
1867
+ } catch (failure) {
1868
+ failures.push(failure);
1869
+ }
1870
+ if (failures.length) {
1871
+ keepRecovery = true;
1872
+ throw new AggregateError([error, ...failures], `Generated output rollback failed. Recovery files remain in: ${staged.map((entry) => entry.dir).join(", ")}`);
1873
+ }
1874
+ throw error;
1875
+ } finally {
1876
+ if (!keepRecovery) await Promise.allSettled(staged.map((entry) => rm(entry.dir, {
1877
+ recursive: true,
1878
+ force: true
1879
+ })));
1880
+ }
1881
+ }
1882
+ //#endregion
1622
1883
  //#region src/node/ogp.ts
1623
1884
  const MAX_OGP_BYTES = 256 * 1024;
1624
1885
  const MAX_OGP_REDIRECTS = 3;
@@ -1804,11 +2065,12 @@ function isHttpUrl(value) {
1804
2065
  }
1805
2066
  //#endregion
1806
2067
  //#region src/node/compile-decks.ts
1807
- async function compileDecks(input) {
2068
+ /** Shared by file output and Vite's in-memory module graph. Does not write files. */
2069
+ async function compileDeckModules(input) {
1808
2070
  const out = normalizeRelativePath(input.out, "Output directory");
1809
2071
  const root = normalizeDeckRoot(input.root);
1810
2072
  const paths = await listFiles(input.cwd, join(input.cwd, root));
1811
- const resolved = resolveDeckFiles(paths, root);
2073
+ const resolved = resolveDeckFiles(paths, root, { markdown: Boolean(input.markdown) });
1812
2074
  const componentModulePaths = Object.fromEntries(resolved.flatMap((deck) => {
1813
2075
  const path = deck.kind === "directory" ? findComponentModule(paths, root, deck.slug) : void 0;
1814
2076
  return path ? [[deck.slug, path]] : [];
@@ -1836,7 +2098,9 @@ async function compileDecks(input) {
1836
2098
  refresh: input.refreshOgp,
1837
2099
  resolveOgp: input.resolveOgp ?? resolveOgpMetadata
1838
2100
  });
1839
- const generated = await compileMdxModuleDecks({
2101
+ const adapter = input.markdown && resolved.some((entry) => entry.sourcePath.endsWith(".md")) ? await loadMarkdownAdapter(input.markdown, input.cwd) : void 0;
2102
+ const generated = await (adapter ? (value) => compileMarkdownDecks(value, adapter) : compileMdxModuleDecks)({
2103
+ cwd: input.cwd,
1840
2104
  root,
1841
2105
  outDir: out,
1842
2106
  mountPath: input.mountPath,
@@ -1848,20 +2112,57 @@ async function compileDecks(input) {
1848
2112
  readText: (path) => readFile(join(input.cwd, path), "utf8"),
1849
2113
  readBinary: (path) => readFile(join(input.cwd, path))
1850
2114
  });
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({
2115
+ let clientCss = "";
2116
+ const clientEntry = await emitClientEntryModule({
1854
2117
  cwd: input.cwd,
1855
2118
  clientEntries: clientEntryPaths,
1856
- clientComponentIds
1857
- }));
1858
- await ogpCache.write();
1859
- return { decks: generated.decks.map((deck) => deck.deck) };
2119
+ clientComponentIds,
2120
+ extraCode: generated.clientCode,
2121
+ clientModules: generated.clientModules,
2122
+ onCss: (css) => {
2123
+ clientCss = css;
2124
+ }
2125
+ });
2126
+ if (clientCss) {
2127
+ 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");
2128
+ generated.routerModule = emitModuleDecksRouter({ decks: generated.decks });
2129
+ }
2130
+ const files = generated.decks.flatMap((deck) => deck.slideModules.map((slide) => ({
2131
+ path: join(input.cwd, slide.path),
2132
+ content: slide.code
2133
+ })));
2134
+ files.push({
2135
+ path: join(input.cwd, out, "decks.ts"),
2136
+ content: generated.routerModule
2137
+ }, {
2138
+ path: join(input.cwd, out, "client-entry.ts"),
2139
+ content: clientEntry
2140
+ });
2141
+ return {
2142
+ modules: files,
2143
+ cacheFile: ogpCache.output(),
2144
+ result: {
2145
+ outDir: out,
2146
+ routerModulePath: `${out}/decks.ts`,
2147
+ decks: generated.decks.map(({ deck, slideModules }) => {
2148
+ const { slides, componentRegistry: _componentRegistry, ...metadata } = deck;
2149
+ return {
2150
+ ...metadata,
2151
+ slides: slides.map((slide, index) => ({
2152
+ index: slide.index,
2153
+ meta: slide.meta,
2154
+ notes: slide.notes,
2155
+ modulePath: slideModules[index].path
2156
+ }))
2157
+ };
2158
+ })
2159
+ }
2160
+ };
1860
2161
  }
1861
2162
  async function createOgpCacheResolver(input) {
1862
2163
  if (!input.cacheFile) return {
1863
2164
  resolveOgp: input.resolveOgp,
1864
- write: async () => void 0
2165
+ output: () => void 0
1865
2166
  };
1866
2167
  const cachePath = join(input.cwd, normalizeRelativePath(input.cacheFile, "OGP cache file"));
1867
2168
  const cache = await readOgpCache(cachePath);
@@ -1877,9 +2178,11 @@ async function createOgpCacheResolver(input) {
1877
2178
  }
1878
2179
  return metadata;
1879
2180
  },
1880
- async write() {
1881
- if (!dirty) return;
1882
- await writeTextFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`);
2181
+ output() {
2182
+ return dirty ? {
2183
+ path: cachePath,
2184
+ content: `${JSON.stringify(cache, null, 2)}\n`
2185
+ } : void 0;
1883
2186
  }
1884
2187
  };
1885
2188
  }
@@ -1909,10 +2212,6 @@ async function readOgpCache(path) {
1909
2212
  function isNodeError(error) {
1910
2213
  return error instanceof Error && "code" in error;
1911
2214
  }
1912
- async function writeTextFile(path, contents) {
1913
- await mkdir(dirname(path), { recursive: true });
1914
- await writeFile(path, contents, "utf8");
1915
- }
1916
2215
  function findComponentModule(paths, root, slug) {
1917
2216
  const base = `${normalizePath(root).replace(/\/$/, "")}/${slug}/components/index`;
1918
2217
  return paths.find((path) => {
@@ -1952,7 +2251,7 @@ function findDeckThemeStyle(paths, root, slug) {
1952
2251
  async function loadDecksConfig(input) {
1953
2252
  const path = resolve(input.cwd, input.configFile ?? "hono-decks.config.ts");
1954
2253
  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({
2254
+ const result = await build({
1956
2255
  absWorkingDir: input.cwd,
1957
2256
  entryPoints: [path],
1958
2257
  bundle: true,
@@ -1960,16 +2259,20 @@ async function loadDecksConfig(input) {
1960
2259
  platform: "node",
1961
2260
  target: "node20",
1962
2261
  write: false,
2262
+ metafile: true,
1963
2263
  logLevel: "silent",
1964
2264
  alias: { "hono-decks": resolveRuntimeEntry() }
1965
- })).outputFiles[0]?.text;
2265
+ });
2266
+ const source = result.outputFiles[0]?.text;
1966
2267
  if (!source) throw new Error(`Could not load config file: ${path}`);
1967
2268
  const config = (await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}#${Date.now()}`)).default;
1968
2269
  if (!config || typeof config !== "object") throw new Error(`Config must have a default export: ${path}`);
1969
2270
  if (typeof config.mountPath !== "string") throw new Error(`Config mountPath must be a string: ${path}`);
2271
+ 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
2272
  return {
1971
2273
  path,
1972
2274
  config,
2275
+ dependencies: Object.keys(result.metafile.inputs).map((inputPath) => resolve(input.cwd, inputPath)),
1973
2276
  root: config.build?.root ?? "decks",
1974
2277
  outDir: config.build?.outDir ?? "src/generated",
1975
2278
  ogpCacheFile: config.build?.ogpCacheFile
@@ -1992,6 +2295,7 @@ function resolveRuntimeEntry() {
1992
2295
  function honoDecks(options = {}) {
1993
2296
  let projectRoot = process.cwd();
1994
2297
  let configPath = resolve(projectRoot, options.configFile ?? "hono-decks.config.ts");
2298
+ let configDependencies = /* @__PURE__ */ new Set();
1995
2299
  let deckRoot = resolve(projectRoot, "decks");
1996
2300
  let resolvedConfig;
1997
2301
  let server;
@@ -1999,9 +2303,17 @@ function honoDecks(options = {}) {
1999
2303
  let timer;
2000
2304
  let compiling = false;
2001
2305
  let queued = false;
2306
+ let compiledMountPath;
2307
+ let mountChanged = false;
2308
+ let modules = /* @__PURE__ */ new Map();
2309
+ let routerModulePath = "";
2002
2310
  const syncWatchedPaths = async () => {
2003
2311
  if (!server) return;
2004
- const next = /* @__PURE__ */ new Set([deckRoot, configPath]);
2312
+ const next = /* @__PURE__ */ new Set([
2313
+ deckRoot,
2314
+ configPath,
2315
+ ...configDependencies
2316
+ ]);
2005
2317
  const removed = [...watchedPaths].filter((path) => !next.has(path));
2006
2318
  if (removed.length > 0) server.watcher.unwatch(removed);
2007
2319
  server.watcher.add([...next]);
@@ -2012,18 +2324,26 @@ function honoDecks(options = {}) {
2012
2324
  cwd: projectRoot,
2013
2325
  configFile: options.configFile
2014
2326
  });
2015
- const manifest = await compileDecks({
2327
+ configDependencies = new Set(loaded.dependencies);
2328
+ await syncWatchedPaths();
2329
+ const compiled = await compileDeckModules({
2016
2330
  cwd: projectRoot,
2017
2331
  root: loaded.root,
2018
2332
  out: loaded.outDir,
2019
2333
  mountPath: loaded.config.mountPath,
2020
2334
  ogpCacheFile: loaded.ogpCacheFile,
2335
+ markdown: loaded.config.build?.markdown,
2021
2336
  refreshOgp: options.refreshOgp
2022
2337
  });
2338
+ if (compiled.cacheFile) await publishGeneratedFiles([compiled.cacheFile]);
2339
+ modules = new Map(compiled.modules.map((file) => [resolve(file.path), file.content]));
2340
+ routerModulePath = resolve(projectRoot, compiled.result.routerModulePath);
2341
+ mountChanged ||= compiledMountPath !== void 0 && compiledMountPath !== loaded.config.mountPath;
2342
+ compiledMountPath = loaded.config.mountPath;
2023
2343
  configPath = loaded.path;
2024
2344
  deckRoot = resolve(projectRoot, loaded.root);
2025
2345
  await syncWatchedPaths();
2026
- resolvedConfig?.logger.info(`[hono-decks] Compiled ${manifest.decks.length} decks to ${loaded.outDir}`);
2346
+ resolvedConfig?.logger.info(`[hono-decks] Compiled ${compiled.result.decks.length} decks as virtual modules`);
2027
2347
  };
2028
2348
  const compile = async (fatal) => {
2029
2349
  if (compiling) {
@@ -2044,7 +2364,18 @@ function honoDecks(options = {}) {
2044
2364
  resolvedConfig?.logger.error(`[hono-decks] ${error instanceof Error ? error.message : String(error)}`);
2045
2365
  }
2046
2366
  } while (queued);
2047
- if (latestCompileSucceeded && server) server.ws.send({ type: "full-reload" });
2367
+ if (latestCompileSucceeded && server) {
2368
+ if (mountChanged) {
2369
+ mountChanged = false;
2370
+ await server.restart();
2371
+ return;
2372
+ }
2373
+ for (const environment of Object.values(server.environments)) {
2374
+ environment.moduleGraph.invalidateAll();
2375
+ if (environment.name !== "client") environment.hot.send({ type: "full-reload" });
2376
+ }
2377
+ server.ws.send({ type: "full-reload" });
2378
+ }
2048
2379
  } finally {
2049
2380
  compiling = false;
2050
2381
  }
@@ -2056,14 +2387,56 @@ function honoDecks(options = {}) {
2056
2387
  const onWatcherEvent = (event, path) => {
2057
2388
  if (event !== "add" && event !== "change" && event !== "unlink") return;
2058
2389
  const absolutePath = resolve(projectRoot, path);
2059
- if (absolutePath === configPath || isWithin(deckRoot, absolutePath)) scheduleCompile();
2390
+ if (absolutePath === configPath || configDependencies.has(absolutePath) || isWithin(deckRoot, absolutePath)) scheduleCompile();
2060
2391
  };
2061
2392
  return {
2062
2393
  name: "hono-decks",
2063
2394
  enforce: "pre",
2395
+ async resolveId(id, importer) {
2396
+ if (id === "virtual:hono-decks") return VIRTUAL_ENTRY;
2397
+ if (id.startsWith(PUBLIC_MODULE_PREFIX)) {
2398
+ const path = decodeURIComponent(id.slice(PUBLIC_MODULE_PREFIX.length));
2399
+ return modules.has(path) ? MODULE_PREFIX + path : null;
2400
+ }
2401
+ if (importer?.startsWith(MODULE_PREFIX)) {
2402
+ const parent = importer.slice(MODULE_PREFIX.length);
2403
+ if (id.startsWith(".")) {
2404
+ const path = resolve(dirname(parent), id);
2405
+ const generated = findModule(modules, path);
2406
+ if (generated) return MODULE_PREFIX + generated;
2407
+ return this.resolve(path, void 0, { skipSelf: true });
2408
+ }
2409
+ return this.resolve(id, resolve(projectRoot, "package.json"), { skipSelf: true });
2410
+ }
2411
+ if (id.startsWith(".") || id.startsWith("/")) {
2412
+ const path = id.startsWith(".") ? resolve(importer ? dirname(importer.split("?")[0]) : projectRoot, id) : id;
2413
+ const generated = findModule(modules, path);
2414
+ if (generated) return MODULE_PREFIX + generated;
2415
+ }
2416
+ return null;
2417
+ },
2418
+ async load(id) {
2419
+ if (id === VIRTUAL_ENTRY) return `import config from ${JSON.stringify(configPath)};
2420
+ import { createDecks } from ${JSON.stringify(PUBLIC_MODULE_PREFIX + encodeURIComponent(routerModulePath))};
2421
+ export { createDecks };
2422
+ export const decks = createDecks(config);`;
2423
+ if (!id.startsWith(MODULE_PREFIX)) return null;
2424
+ const path = id.slice(MODULE_PREFIX.length);
2425
+ const code = modules.get(path);
2426
+ if (code === void 0) return null;
2427
+ return (await transform(code, {
2428
+ loader: "ts",
2429
+ format: "esm",
2430
+ sourcefile: path,
2431
+ sourcemap: "inline"
2432
+ })).code;
2433
+ },
2434
+ config() {
2435
+ return { server: { watch: { ignored: [/(?:^|[/\\])\.hono-decks-[^/\\]+(?:[/\\]|$)/] } } };
2436
+ },
2064
2437
  async configResolved(config) {
2065
2438
  resolvedConfig = config;
2066
- projectRoot = config.root;
2439
+ projectRoot = config.resolve?.preserveSymlinks ? config.root : await realpath(config.root);
2067
2440
  configPath = resolve(projectRoot, options.configFile ?? "hono-decks.config.ts");
2068
2441
  await compile(true);
2069
2442
  },
@@ -2081,9 +2454,24 @@ function honoDecks(options = {}) {
2081
2454
  }
2082
2455
  };
2083
2456
  }
2457
+ const VIRTUAL_ENTRY = "\0hono-decks:entry";
2458
+ const PUBLIC_MODULE_PREFIX = "virtual:hono-decks/module/";
2459
+ const MODULE_PREFIX = "\0hono-decks:module:";
2460
+ function findModule(modules, path) {
2461
+ return [
2462
+ path,
2463
+ path.replace(/\.js$/, ".ts"),
2464
+ path + ".ts",
2465
+ path + "/index.ts"
2466
+ ].find((candidate) => modules.has(candidate));
2467
+ }
2084
2468
  function isWithin(root, path) {
2085
2469
  const child = relative(root, path);
2086
2470
  return child === "" || child !== ".." && !child.startsWith("../") && !child.startsWith("..\\");
2087
2471
  }
2472
+ /** Add deck compilation to an existing object, promise, or config function. */
2473
+ function withHonoDecks(config, options = {}) {
2474
+ return async (env) => mergeConfig(await (typeof config === "function" ? config(env) : config), { plugins: [honoDecks(options)] });
2475
+ }
2088
2476
  //#endregion
2089
- export { honoDecks };
2477
+ export { honoDecks, withHonoDecks };