hono-decks 0.1.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 ADDED
@@ -0,0 +1,2175 @@
1
+ // src/vite.ts
2
+ import { relative as relative3, resolve as resolve2 } from "path";
3
+
4
+ // src/node/compile-decks.ts
5
+ import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
6
+ import { join as join3 } from "path";
7
+
8
+ // src/routing/file-routing.ts
9
+ function resolveDeckFiles(paths, root = "decks") {
10
+ const normalizedRoot = normalizePath(root).replace(/\/$/, "");
11
+ const decks = /* @__PURE__ */ new Map();
12
+ const assetsBySlug = /* @__PURE__ */ new Map();
13
+ for (const inputPath of paths.map(normalizePath).sort()) {
14
+ assertInsideRoot(inputPath, normalizedRoot);
15
+ const relative4 = inputPath.slice(normalizedRoot.length + 1);
16
+ const segments = relative4.split("/");
17
+ if (segments.length === 1 && segments[0].endsWith(".mdx")) {
18
+ const slug = segments[0].replace(/\.mdx$/, "");
19
+ addDeck(decks, {
20
+ slug,
21
+ sourcePath: inputPath,
22
+ kind: "single-file",
23
+ assetPaths: []
24
+ });
25
+ continue;
26
+ }
27
+ if (segments.length === 2 && segments[1] === "deck.mdx") {
28
+ addDeck(decks, {
29
+ slug: segments[0],
30
+ sourcePath: inputPath,
31
+ kind: "directory",
32
+ assetPaths: assetsBySlug.get(segments[0]) ?? []
33
+ });
34
+ continue;
35
+ }
36
+ if (segments.length >= 3 && segments[1] === "assets") {
37
+ const slug = segments[0];
38
+ const assetPaths = assetsBySlug.get(slug) ?? [];
39
+ assetPaths.push(inputPath);
40
+ assetsBySlug.set(slug, assetPaths);
41
+ const deck = decks.get(slug);
42
+ if (deck?.kind === "directory") deck.assetPaths = assetPaths;
43
+ continue;
44
+ }
45
+ if (inputPath.endsWith(".mdx")) {
46
+ throw new Error(`Nested deck slugs are not supported in this slice: ${inputPath}`);
47
+ }
48
+ }
49
+ return [...decks.values()].map((deck) => ({
50
+ ...deck,
51
+ assetPaths: [...deck.assetPaths].sort()
52
+ }));
53
+ }
54
+ function addDeck(decks, next) {
55
+ const current = decks.get(next.slug);
56
+ if (current) {
57
+ throw new Error(`Deck slug conflict for "${next.slug}": ${current.sourcePath} and ${next.sourcePath}`);
58
+ }
59
+ decks.set(next.slug, next);
60
+ }
61
+ function assertInsideRoot(path, root) {
62
+ if (path.includes("/../") || path.endsWith("/..") || path.startsWith("../")) {
63
+ throw new Error(`Deck path escapes the root: ${path}`);
64
+ }
65
+ if (path !== root && !path.startsWith(`${root}/`)) {
66
+ throw new Error(`Deck path is outside ${root}: ${path}`);
67
+ }
68
+ }
69
+ function normalizePath(path) {
70
+ return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
71
+ }
72
+
73
+ // src/deck/model.ts
74
+ var SLIDE_TRANSITIONS = [
75
+ "none",
76
+ "fade",
77
+ "fade-out",
78
+ "slide-left",
79
+ "slide-right",
80
+ "slide-up",
81
+ "slide-down",
82
+ "view-transition"
83
+ ];
84
+ var CompileError = class extends Error {
85
+ constructor(message, code) {
86
+ super(message);
87
+ this.code = code;
88
+ this.name = "CompileError";
89
+ }
90
+ };
91
+
92
+ // src/deck/frontmatter.ts
93
+ function readFrontmatter(source) {
94
+ const normalized = source.replace(/\r\n/g, "\n").trimStart();
95
+ if (!normalized.startsWith("---\n")) return { attrs: {}, body: source.trim() };
96
+ const end = normalized.indexOf("\n---", 4);
97
+ if (end === -1) throw new CompileError("Frontmatter block is not closed.", "frontmatter-unclosed");
98
+ const rawAttrs = normalized.slice(4, end).trim();
99
+ const body = normalized.slice(end + 4).replace(/^\n/, "").trim();
100
+ return { attrs: parseFrontmatterAttrs(rawAttrs), body };
101
+ }
102
+ function toDeckFrontmatter(attrs, warnings) {
103
+ const meta = { ...attrs };
104
+ const deck = { meta };
105
+ deck.title = takeString(meta, "title");
106
+ deck.description = takeString(meta, "description");
107
+ deck.author = takeString(meta, "author");
108
+ deck.date = takeString(meta, "date");
109
+ deck.theme = takeString(meta, "theme");
110
+ deck.transition = takeKnownStringWithWarning(meta, "transition", SLIDE_TRANSITIONS, "none", warnings, "unknown-transition");
111
+ deck.transitionDuration = takeTransitionDuration(meta, warnings);
112
+ deck.transitionEasing = takeTransitionEasing(meta, warnings);
113
+ deck.assets = takeStringOrStringArray(meta, "assets");
114
+ deck.draft = takeBoolean(meta, "draft");
115
+ deck.presenter = takeBoolean(meta, "presenter");
116
+ const tags = meta.tags;
117
+ if (Array.isArray(tags)) {
118
+ deck.tags = tags.map(String);
119
+ delete meta.tags;
120
+ }
121
+ return deck;
122
+ }
123
+ function toSlideFrontmatter(attrs, warnings, input) {
124
+ const meta = { ...attrs };
125
+ const slide = {
126
+ title: takeString(meta, "title") ?? input.fallbackTitle,
127
+ layout: takeString(meta, "layout") ?? input.fallbackLayout,
128
+ className: takeString(meta, "class") ?? input.fallbackClassName,
129
+ notes: takeString(meta, "notes"),
130
+ background: takeString(meta, "background"),
131
+ transition: takeKnownStringWithWarning(
132
+ meta,
133
+ "transition",
134
+ SLIDE_TRANSITIONS,
135
+ "none",
136
+ warnings,
137
+ "unknown-transition",
138
+ input.slideIndex
139
+ ) ?? input.fallbackTransition,
140
+ transitionDuration: takeTransitionDuration(meta, warnings, input.slideIndex) ?? input.fallbackTransitionDuration,
141
+ transitionEasing: takeTransitionEasing(meta, warnings, input.slideIndex) ?? input.fallbackTransitionEasing,
142
+ meta
143
+ };
144
+ return slide;
145
+ }
146
+ function addUnknownFrontmatterWarnings(warnings, meta, scope, slideIndex) {
147
+ for (const key of Object.keys(meta)) {
148
+ warnings.push({
149
+ code: "unknown-frontmatter-key",
150
+ message: `Unknown ${scope} frontmatter key "${key}" is preserved in meta.`,
151
+ ...slideIndex !== void 0 ? { slideIndex } : {}
152
+ });
153
+ }
154
+ }
155
+ function splitSlideSources(source) {
156
+ const lines = source.replace(/\r\n/g, "\n").trim().split("\n");
157
+ const slides = [];
158
+ let current = [];
159
+ let cursor = 0;
160
+ while (cursor < lines.length) {
161
+ if (isFence(lines[cursor])) {
162
+ if (looksLikeFrontmatterFence(lines, cursor) && findFrontmatterEnd(lines, cursor) === -1) {
163
+ throw new CompileError("Frontmatter block is not closed.", "frontmatter-unclosed");
164
+ }
165
+ if (isFrontmatterStart(lines, cursor)) {
166
+ if (hasMeaningfulLines(current)) {
167
+ slides.push(current.join("\n").trim());
168
+ }
169
+ current = [];
170
+ const frontmatterEnd = findFrontmatterEnd(lines, cursor);
171
+ current.push(...lines.slice(cursor, frontmatterEnd + 1));
172
+ cursor = frontmatterEnd + 1;
173
+ continue;
174
+ }
175
+ if (hasMeaningfulLines(current)) {
176
+ slides.push(current.join("\n").trim());
177
+ current = [];
178
+ }
179
+ cursor += 1;
180
+ continue;
181
+ }
182
+ current.push(lines[cursor]);
183
+ cursor += 1;
184
+ }
185
+ if (hasMeaningfulLines(current)) {
186
+ slides.push(current.join("\n").trim());
187
+ }
188
+ return slides;
189
+ }
190
+ function parseFrontmatterAttrs(source) {
191
+ const attrs = {};
192
+ const lines = source.split("\n");
193
+ for (let index = 0; index < lines.length; index += 1) {
194
+ const line = lines[index];
195
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
196
+ if (!match) {
197
+ if (line.trim()) {
198
+ throw new CompileError(`Invalid frontmatter line ${index + 1}: "${line.trim()}"`, "frontmatter-invalid-line");
199
+ }
200
+ continue;
201
+ }
202
+ const key = match[1];
203
+ const value = match[2];
204
+ if (value === "|") {
205
+ const block = [];
206
+ index += 1;
207
+ while (index < lines.length && /^\s+/.test(lines[index])) {
208
+ block.push(lines[index].trim());
209
+ index += 1;
210
+ }
211
+ index -= 1;
212
+ attrs[key] = block.join("\n").trim();
213
+ continue;
214
+ }
215
+ if (value.trim() === "") {
216
+ const nested = [];
217
+ index += 1;
218
+ while (index < lines.length && /^\s+/.test(lines[index])) {
219
+ nested.push(lines[index]);
220
+ index += 1;
221
+ }
222
+ index -= 1;
223
+ attrs[key] = parseNestedFrontmatterValue(nested);
224
+ continue;
225
+ }
226
+ attrs[key] = parseScalar(value);
227
+ }
228
+ return attrs;
229
+ }
230
+ function isFrontmatterStart(lines, index) {
231
+ return looksLikeFrontmatterFence(lines, index) && findFrontmatterEnd(lines, index) > index;
232
+ }
233
+ function looksLikeFrontmatterFence(lines, index) {
234
+ if (!isFence(lines[index])) return false;
235
+ const next = lines[index + 1];
236
+ return next != null && /^([A-Za-z_][A-Za-z0-9_-]*):\s*/.test(next);
237
+ }
238
+ function findFrontmatterEnd(lines, start) {
239
+ for (let index = start + 1; index < lines.length; index += 1) {
240
+ if (isFence(lines[index])) return index;
241
+ }
242
+ return -1;
243
+ }
244
+ function isFence(line) {
245
+ return /^---\s*$/.test(line);
246
+ }
247
+ function hasMeaningfulLines(lines) {
248
+ return lines.some((line) => line.trim() !== "");
249
+ }
250
+ function parseNestedFrontmatterValue(lines) {
251
+ const meaningful = lines.map((line) => line.trim()).filter(Boolean);
252
+ if (meaningful.every((line) => line.startsWith("- "))) {
253
+ return meaningful.map((line) => parseScalar(line.slice(2)));
254
+ }
255
+ const object = {};
256
+ for (const line of meaningful) {
257
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
258
+ if (!match) {
259
+ throw new CompileError(`Invalid nested frontmatter line: "${line}"`, "frontmatter-invalid-line");
260
+ }
261
+ object[match[1]] = parseScalar(match[2]);
262
+ }
263
+ return object;
264
+ }
265
+ function parseScalar(value) {
266
+ const trimmed = value.trim().replace(/^['"]|['"]$/g, "");
267
+ if (trimmed === "true") return true;
268
+ if (trimmed === "false") return false;
269
+ if (/^\[.*\]$/.test(trimmed)) {
270
+ return trimmed.slice(1, -1).split(",").map((item) => item.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
271
+ }
272
+ return trimmed;
273
+ }
274
+ function takeString(attrs, key) {
275
+ const value = attrs[key];
276
+ if (typeof value !== "string") return void 0;
277
+ delete attrs[key];
278
+ return value;
279
+ }
280
+ function takeKnownStringWithWarning(attrs, key, values, fallback, warnings, code, slideIndex) {
281
+ const value = attrs[key];
282
+ delete attrs[key];
283
+ if (value === void 0) return void 0;
284
+ if (typeof value === "string" && values.includes(value)) return value;
285
+ warnings.push({
286
+ code,
287
+ message: `Unknown ${key} value "${String(value)}"; using ${fallback}.`,
288
+ ...slideIndex !== void 0 ? { slideIndex } : {}
289
+ });
290
+ return fallback;
291
+ }
292
+ function takeTransitionDuration(attrs, warnings, slideIndex) {
293
+ const value = attrs.transitionDuration;
294
+ delete attrs.transitionDuration;
295
+ if (value === void 0) return void 0;
296
+ if (typeof value === "string" && isValidTransitionDuration(value)) return value;
297
+ warnings.push({
298
+ code: "invalid-transition-duration",
299
+ message: `Invalid transitionDuration value "${String(value)}"; ignoring it.`,
300
+ ...slideIndex !== void 0 ? { slideIndex } : {}
301
+ });
302
+ return void 0;
303
+ }
304
+ function takeTransitionEasing(attrs, warnings, slideIndex) {
305
+ const value = attrs.transitionEasing;
306
+ delete attrs.transitionEasing;
307
+ if (value === void 0) return void 0;
308
+ if (typeof value === "string" && isValidTransitionEasing(value)) return value;
309
+ warnings.push({
310
+ code: "invalid-transition-easing",
311
+ message: `Invalid transitionEasing value "${String(value)}"; ignoring it.`,
312
+ ...slideIndex !== void 0 ? { slideIndex } : {}
313
+ });
314
+ return void 0;
315
+ }
316
+ function isValidTransitionDuration(value) {
317
+ return value.split(",").map((item) => item.trim()).every((item) => /^(?:\d+|\d*\.\d+)(?:ms|s)$/.test(item));
318
+ }
319
+ function isValidTransitionEasing(value) {
320
+ const easing = value.trim();
321
+ return ["linear", "ease", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end"].includes(easing) || /^cubic-bezier\(\s*-?(?:\d+|\d*\.\d+)\s*,\s*-?(?:\d+|\d*\.\d+)\s*,\s*-?(?:\d+|\d*\.\d+)\s*,\s*-?(?:\d+|\d*\.\d+)\s*\)$/.test(
322
+ easing
323
+ ) || /^steps\(\s*\d+\s*(?:,\s*(?:jump-start|jump-end|jump-none|jump-both|start|end))?\s*\)$/.test(easing) || /^linear\([^)]+\)$/.test(easing);
324
+ }
325
+ function takeStringOrStringArray(attrs, key) {
326
+ const value = attrs[key];
327
+ if (typeof value === "string") {
328
+ delete attrs[key];
329
+ return value;
330
+ }
331
+ if (Array.isArray(value)) {
332
+ delete attrs[key];
333
+ return value.map(String);
334
+ }
335
+ return void 0;
336
+ }
337
+ function takeBoolean(attrs, key) {
338
+ const value = attrs[key];
339
+ if (typeof value !== "boolean") return void 0;
340
+ delete attrs[key];
341
+ return value;
342
+ }
343
+
344
+ // src/deck/assets.ts
345
+ function buildExternalAssetRefs(candidates) {
346
+ const refs = /* @__PURE__ */ new Map();
347
+ for (const candidate of candidates) {
348
+ const type = assetRefType(candidate);
349
+ if (!type || refs.has(candidate)) continue;
350
+ const contentType = contentTypeForPath(candidate);
351
+ refs.set(candidate, {
352
+ sourcePath: candidate,
353
+ publicPath: candidate,
354
+ type,
355
+ ...contentType ? { contentType } : {}
356
+ });
357
+ }
358
+ return [...refs.values()];
359
+ }
360
+ function addExternalAssetWarnings(warnings, assets) {
361
+ for (const asset of assets) {
362
+ if (asset.type !== "remote" && asset.type !== "r2") continue;
363
+ const label = asset.type === "r2" ? "R2" : "Remote";
364
+ warnings.push({
365
+ code: "external-asset-unverified",
366
+ message: `${label} asset existence cannot be verified at compile time: ${asset.sourcePath}`
367
+ });
368
+ }
369
+ }
370
+ function collectMarkdownAssetCandidates(markdown) {
371
+ const candidates = [];
372
+ for (const match of markdown.matchAll(/^\s*(?:background|image|src|asset):\s*['"]?([^'"\n]+)['"]?\s*$/gim)) {
373
+ candidates.push(match[1].trim());
374
+ }
375
+ for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) {
376
+ candidates.push(match[1].trim());
377
+ }
378
+ for (const match of markdown.matchAll(/\b(?:src|href|image|background)=["']([^"']+)["']/g)) {
379
+ candidates.push(match[1].trim());
380
+ }
381
+ return candidates;
382
+ }
383
+ function collectFrontmatterAssetCandidates(value) {
384
+ if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
385
+ if (typeof value === "string") return [value.trim()].filter(Boolean);
386
+ return [];
387
+ }
388
+ function contentTypeForPath(path) {
389
+ const pathname = path.replace(/\?.*$/, "").toLowerCase();
390
+ if (/\.png(?:#.*)?$/.test(pathname)) return "image/png";
391
+ if (/\.jpe?g(?:#.*)?$/.test(pathname)) return "image/jpeg";
392
+ if (/\.gif(?:#.*)?$/.test(pathname)) return "image/gif";
393
+ if (/\.svg(?:#.*)?$/.test(pathname)) return "image/svg+xml";
394
+ if (/\.webp(?:#.*)?$/.test(pathname)) return "image/webp";
395
+ return void 0;
396
+ }
397
+ function assetRefType(value) {
398
+ if (/^https?:\/\//i.test(value)) return "remote";
399
+ if (/^r2:\/\//i.test(value)) return "r2";
400
+ if (value.startsWith("/")) return "public";
401
+ return void 0;
402
+ }
403
+
404
+ // src/deck/speaker-notes.ts
405
+ function extractMdxCommentSpeakerNotes(source) {
406
+ const notes = [];
407
+ const body = source.replace(/\{\/\*([\s\S]*?)\*\/\}/g, (_match, rawNote) => {
408
+ const note = normalizeSpeakerNote(rawNote);
409
+ if (note) notes.push(note);
410
+ return "";
411
+ });
412
+ return {
413
+ body,
414
+ ...notes.length ? { notes: notes.join("\n\n") } : {}
415
+ };
416
+ }
417
+ function combineSpeakerNotes(...notes) {
418
+ const combined = notes.map((note) => note?.trim()).filter((note) => Boolean(note));
419
+ return combined.length ? combined.join("\n\n") : void 0;
420
+ }
421
+ function normalizeSpeakerNote(value) {
422
+ return value.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).join("\n").trim();
423
+ }
424
+
425
+ // src/parser/parser.ts
426
+ import { unified } from "unified";
427
+ import remarkGfm from "remark-gfm";
428
+ import remarkMdx from "remark-mdx";
429
+ import remarkParse from "remark-parse";
430
+ var slideSeparator = /^---\s*$/m;
431
+ function parseDeckWithWarnings(markdown) {
432
+ const normalized = markdown.replace(/\r\n/g, "\n").trim();
433
+ const chunks = normalized.length > 0 ? normalized.split(slideSeparator) : [""];
434
+ const parsedSlides = chunks.map((chunk, index) => parseSlide(chunk.trim(), index)).filter(({ slide }) => slide.raw.length > 0 || slide.blocks.length > 0 || slide.nodes.length > 0);
435
+ return {
436
+ slides: parsedSlides.map(({ slide }) => slide),
437
+ warnings: parsedSlides.flatMap(({ warnings }) => warnings)
438
+ };
439
+ }
440
+ function parseSlide(source, index) {
441
+ const { attrs, body } = readSlideAttributes(source);
442
+ const parsed = parseContent(body, index);
443
+ const firstHeading = parsed.blocks.find((block) => block.type === "heading");
444
+ return {
445
+ slide: {
446
+ index,
447
+ title: stringAttr(attrs.title) ?? firstHeading?.text,
448
+ layout: stringAttr(attrs.layout) ?? (index === 0 ? "cover" : "default"),
449
+ className: stringAttr(attrs.class),
450
+ blocks: parsed.blocks,
451
+ nodes: parsed.nodes,
452
+ raw: source
453
+ },
454
+ warnings: parsed.warnings
455
+ };
456
+ }
457
+ function readSlideAttributes(source) {
458
+ const lines = source.split("\n");
459
+ const attrs = {};
460
+ let cursor = 0;
461
+ while (cursor < lines.length) {
462
+ const line = lines[cursor];
463
+ const match = /^(title|layout|class):\s*(.+)$/.exec(line);
464
+ if (!match) break;
465
+ attrs[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
466
+ cursor += 1;
467
+ }
468
+ return { attrs, body: lines.slice(cursor).join("\n").trim() };
469
+ }
470
+ function parseContent(body, slideIndex) {
471
+ const { root, warnings: parseWarnings } = parseMarkdownTree(body, slideIndex);
472
+ const blockResults = (root.children ?? []).map((child) => toBlocks(child, slideIndex, body));
473
+ const nodeResults = (root.children ?? []).map((child) => toNodes(child, slideIndex, body));
474
+ return {
475
+ blocks: blockResults.flatMap(({ blocks }) => blocks),
476
+ nodes: nodeResults.flatMap(({ nodes }) => nodes),
477
+ warnings: [
478
+ ...fenceWarningsFor(body, slideIndex),
479
+ ...parseWarnings,
480
+ ...blockResults.flatMap(({ warnings }) => warnings),
481
+ ...nodeResults.flatMap(({ warnings }) => warnings)
482
+ ]
483
+ };
484
+ }
485
+ function parseMarkdownTree(body, slideIndex) {
486
+ try {
487
+ return {
488
+ root: unified().use(remarkParse).use(remarkGfm).use(remarkMdx).parse(body),
489
+ warnings: []
490
+ };
491
+ } catch (error) {
492
+ const message = error instanceof Error ? error.message : "Unknown MDX parse error";
493
+ return {
494
+ root: unified().use(remarkParse).use(remarkGfm).parse(body),
495
+ warnings: [parserWarning("mdx-parse-error", `Slide ${slideIndex + 1}: ${message}`, slideIndex)]
496
+ };
497
+ }
498
+ }
499
+ function fenceWarningsFor(body, slideIndex) {
500
+ let openFence;
501
+ for (const line of body.split("\n")) {
502
+ const match = /^(```|~~~)/.exec(line);
503
+ if (!match) continue;
504
+ if (!openFence) {
505
+ openFence = match[1];
506
+ continue;
507
+ }
508
+ if (line.startsWith(openFence)) openFence = void 0;
509
+ }
510
+ if (!openFence) return [];
511
+ return [parserWarning("code-fence-unclosed", `Slide ${slideIndex + 1}: code fence is not closed.`, slideIndex)];
512
+ }
513
+ function toBlocks(node, slideIndex, source) {
514
+ switch (node.type) {
515
+ case "heading":
516
+ return blockResult([{ type: "heading", depth: headingDepth(node.depth), text: textContent(node) }]);
517
+ case "paragraph": {
518
+ const image = soleImage(node);
519
+ if (image) {
520
+ return blockResult([
521
+ {
522
+ type: "image",
523
+ alt: image.alt ?? "",
524
+ src: image.url ?? "",
525
+ ...image.title ? { title: image.title } : {}
526
+ }
527
+ ]);
528
+ }
529
+ return blockResult([{ type: "paragraph", text: textContent(node).replace(/\s+/g, " ").trim() }]);
530
+ }
531
+ case "list":
532
+ return blockResult([
533
+ {
534
+ type: "list",
535
+ ordered: node.ordered === true,
536
+ items: (node.children ?? []).map((item) => taskListItemText(item)).filter(Boolean)
537
+ }
538
+ ]);
539
+ case "code":
540
+ return blockResult([{ type: "code", lang: node.lang ?? void 0, code: node.value ?? "" }]);
541
+ case "blockquote":
542
+ return blockResult([{ type: "blockquote", text: textContent(node).trim() }]);
543
+ case "table":
544
+ return blockResult([tableBlock(node)]);
545
+ case "mdxJsxFlowElement":
546
+ case "mdxJsxTextElement":
547
+ return jsxElementBlock(node, slideIndex, source);
548
+ case "mdxjsEsm":
549
+ case "mdxFlowExpression":
550
+ case "mdxTextExpression":
551
+ case "thematicBreak":
552
+ return blockResult([]);
553
+ default:
554
+ return blockResult(textContent(node).trim() ? [{ type: "paragraph", text: textContent(node).trim() }] : []);
555
+ }
556
+ }
557
+ function jsxElementBlock(node, slideIndex, source) {
558
+ const name = node.name ?? "";
559
+ if (!name) return blockResult([]);
560
+ if (isComponentName(name)) {
561
+ const parsedProps = parseProps(node.attributes ?? [], slideIndex, name);
562
+ return blockResult(
563
+ [
564
+ {
565
+ type: "component",
566
+ name,
567
+ props: parsedProps.props,
568
+ raw: sourceForNode(node, source) ?? `<${name} />`
569
+ }
570
+ ],
571
+ parsedProps.warnings
572
+ );
573
+ }
574
+ return blockResult([{ type: "paragraph", text: textContent(node).trim() }]);
575
+ }
576
+ function toNodes(node, slideIndex, source) {
577
+ switch (node.type) {
578
+ case "text":
579
+ return nodeResult(node.value ? [{ type: "text", value: node.value }] : []);
580
+ case "emphasis":
581
+ return elementNodeResult(node, "em", slideIndex, source);
582
+ case "strong":
583
+ return elementNodeResult(node, "strong", slideIndex, source);
584
+ case "delete":
585
+ return elementNodeResult(node, "del", slideIndex, source);
586
+ case "inlineCode":
587
+ return nodeResult([{ type: "element", tag: "code", props: {}, children: [{ type: "text", value: node.value ?? "" }] }]);
588
+ case "break":
589
+ return nodeResult([{ type: "element", tag: "br", props: {}, children: [] }]);
590
+ case "heading":
591
+ return elementNodeResult(node, `h${headingDepth(node.depth)}`, slideIndex, source);
592
+ case "paragraph": {
593
+ if (soleImage(node)) return childrenToNodes(node, slideIndex, source);
594
+ if (isJsxOnlyParagraph(node)) return childrenToNodes(node, slideIndex, source);
595
+ return elementNodeResult(node, "p", slideIndex, source);
596
+ }
597
+ case "list": {
598
+ const tag = node.ordered === true ? "ol" : "ul";
599
+ const items = node.children ?? [];
600
+ const childResults = items.map((child) => childrenToNodes(child, slideIndex, source));
601
+ return nodeResult(
602
+ [
603
+ {
604
+ type: "element",
605
+ tag,
606
+ props: {},
607
+ children: childResults.map(({ nodes }, itemIndex) => listItemNode(nodes, items[itemIndex]?.checked))
608
+ }
609
+ ],
610
+ childResults.flatMap(({ warnings }) => warnings)
611
+ );
612
+ }
613
+ case "listItem":
614
+ return childrenToNodes(node, slideIndex, source);
615
+ case "code":
616
+ return nodeResult([{ type: "code", lang: node.lang ?? void 0, value: node.value ?? "" }]);
617
+ case "blockquote":
618
+ return elementNodeResult(node, "blockquote", slideIndex, source);
619
+ case "image":
620
+ return nodeResult([
621
+ {
622
+ type: "element",
623
+ tag: "img",
624
+ props: { src: node.url ?? "", alt: node.alt ?? "", ...node.title ? { title: node.title } : {} },
625
+ children: []
626
+ }
627
+ ]);
628
+ case "link":
629
+ return elementNodeResult(node, "a", slideIndex, source, { href: node.url ?? "" });
630
+ case "table":
631
+ return tableNodes(node, slideIndex, source);
632
+ case "mdxJsxFlowElement":
633
+ case "mdxJsxTextElement":
634
+ return jsxElementNodes(node, slideIndex, source);
635
+ case "mdxjsEsm":
636
+ return nodeResult(
637
+ [],
638
+ [
639
+ parserWarning(
640
+ "mdx-import-export-ignored",
641
+ `Slide ${slideIndex + 1}: MDX import/export syntax is ignored.`,
642
+ slideIndex
643
+ )
644
+ ]
645
+ );
646
+ case "mdxFlowExpression":
647
+ case "mdxTextExpression":
648
+ return nodeResult(
649
+ [],
650
+ [
651
+ parserWarning(
652
+ "mdx-expression-ignored",
653
+ `Slide ${slideIndex + 1}: MDX JavaScript expressions are ignored.`,
654
+ slideIndex
655
+ )
656
+ ]
657
+ );
658
+ case "thematicBreak":
659
+ return nodeResult([{ type: "element", tag: "hr", props: {}, children: [] }]);
660
+ default:
661
+ return childrenToNodes(node, slideIndex, source);
662
+ }
663
+ }
664
+ function elementNodeResult(node, tag, slideIndex, source, props = {}) {
665
+ const children = childrenToNodes(node, slideIndex, source);
666
+ return nodeResult(
667
+ [
668
+ {
669
+ type: "element",
670
+ tag,
671
+ props,
672
+ children: children.nodes
673
+ }
674
+ ],
675
+ children.warnings
676
+ );
677
+ }
678
+ function listItemNode(children, checked) {
679
+ if (typeof checked !== "boolean") return { type: "element", tag: "li", props: {}, children };
680
+ return {
681
+ type: "element",
682
+ tag: "li",
683
+ props: { class: "task-list-item" },
684
+ children: [
685
+ {
686
+ type: "element",
687
+ tag: "input",
688
+ props: { type: "checkbox", disabled: true, ...checked ? { checked: true } : {} },
689
+ children: []
690
+ },
691
+ { type: "text", value: " " },
692
+ ...children
693
+ ]
694
+ };
695
+ }
696
+ function tableNodes(node, slideIndex, source) {
697
+ const align = node.align ?? [];
698
+ const [headerRow, ...bodyRows] = node.children ?? [];
699
+ const headerResult = headerRow ? tableRowCells(headerRow, "th", align, slideIndex, source) : cellsResult([]);
700
+ const bodyResults = bodyRows.map((row) => tableRowCells(row, "td", align, slideIndex, source));
701
+ return nodeResult(
702
+ [
703
+ {
704
+ type: "element",
705
+ tag: "table",
706
+ props: {},
707
+ children: [
708
+ {
709
+ type: "element",
710
+ tag: "thead",
711
+ props: {},
712
+ children: [{ type: "element", tag: "tr", props: {}, children: headerResult.cells }]
713
+ },
714
+ {
715
+ type: "element",
716
+ tag: "tbody",
717
+ props: {},
718
+ children: bodyResults.map(({ cells }) => ({ type: "element", tag: "tr", props: {}, children: cells }))
719
+ }
720
+ ]
721
+ }
722
+ ],
723
+ [...headerResult.warnings, ...bodyResults.flatMap(({ warnings }) => warnings)]
724
+ );
725
+ }
726
+ function cellsResult(cells, emittedWarnings = []) {
727
+ return { cells, warnings: emittedWarnings };
728
+ }
729
+ function tableRowCells(row, tag, align, slideIndex, source) {
730
+ const results = (row.children ?? []).map((cell, cellIndex) => {
731
+ const children = childrenToNodes(cell, slideIndex, source);
732
+ const cellAlign = toTableAlign(align[cellIndex]);
733
+ return {
734
+ cell: {
735
+ type: "element",
736
+ tag,
737
+ props: cellAlign ? { style: `text-align:${cellAlign}` } : {},
738
+ children: children.nodes
739
+ },
740
+ warnings: children.warnings
741
+ };
742
+ });
743
+ return cellsResult(
744
+ results.map(({ cell }) => cell),
745
+ results.flatMap(({ warnings }) => warnings)
746
+ );
747
+ }
748
+ function jsxElementNodes(node, slideIndex, source) {
749
+ const name = node.name ?? "";
750
+ if (!name) return childrenToNodes(node, slideIndex, source);
751
+ if (isUnsafeHtmlElement(name)) {
752
+ return nodeResult([{ type: "text", value: sourceForNode(node, source) ?? textContent(node) }]);
753
+ }
754
+ const parsedProps = parseProps(node.attributes ?? [], slideIndex, name);
755
+ const parsedChildren = childrenToNodes(node, slideIndex, source);
756
+ const children = parsedChildren.nodes.filter((child) => child.type !== "text" || child.value.trim() !== "");
757
+ const warnings = [...parsedProps.warnings, ...parsedChildren.warnings];
758
+ if (isComponentName(name)) return nodeResult([{ type: "component", name, props: parsedProps.props, children }], warnings);
759
+ return nodeResult([{ type: "element", tag: name, props: parsedProps.props, children }], warnings);
760
+ }
761
+ function childrenToNodes(node, slideIndex, source) {
762
+ const results = (node.children ?? []).map((child) => toNodes(child, slideIndex, source));
763
+ return nodeResult(
764
+ results.flatMap(({ nodes }) => nodes),
765
+ results.flatMap(({ warnings }) => warnings)
766
+ );
767
+ }
768
+ function parseProps(attributes, slideIndex, componentName) {
769
+ const props = {};
770
+ const warnings = attributes.flatMap((attr) => {
771
+ if (attr.type !== "mdxJsxAttribute" || !attr.name) return [];
772
+ if (attr.value === null || attr.value === void 0) {
773
+ props[attr.name] = true;
774
+ return [];
775
+ }
776
+ if (typeof attr.value === "string" || typeof attr.value === "number" || typeof attr.value === "boolean") {
777
+ props[attr.name] = attr.value;
778
+ return [];
779
+ }
780
+ return [
781
+ parserWarning(
782
+ "mdx-expression-prop-ignored",
783
+ `Slide ${slideIndex + 1}: MDX JavaScript expression props are ignored on ${ignoredPropTarget(componentName, attr.name)}.`,
784
+ slideIndex
785
+ )
786
+ ];
787
+ });
788
+ return { props, warnings };
789
+ }
790
+ function blockResult(blocks, emittedWarnings = []) {
791
+ return { blocks, warnings: emittedWarnings };
792
+ }
793
+ function nodeResult(nodes, emittedWarnings = []) {
794
+ return { nodes, warnings: emittedWarnings };
795
+ }
796
+ function parserWarning(code, message, slideIndex) {
797
+ return { code, message, slideIndex };
798
+ }
799
+ function ignoredPropTarget(componentName, propName) {
800
+ if (propName.startsWith("$")) return `${componentName} dynamic prop`;
801
+ return `${componentName}.${propName}`;
802
+ }
803
+ function textContent(node) {
804
+ if (typeof node.value === "string") return node.value;
805
+ if (node.type === "strong") return `**${(node.children ?? []).map(textContent).join("")}**`;
806
+ if (node.type === "emphasis") return `*${(node.children ?? []).map(textContent).join("")}*`;
807
+ if (node.type === "delete") return `~~${(node.children ?? []).map(textContent).join("")}~~`;
808
+ if (node.type === "inlineCode") return `\`${node.value ?? ""}\``;
809
+ if ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && node.name && !isComponentName(node.name)) {
810
+ return `<${node.name}>${(node.children ?? []).map(textContent).join("")}</${node.name}>`;
811
+ }
812
+ return (node.children ?? []).map(textContent).join("");
813
+ }
814
+ function taskListItemText(item) {
815
+ const text = textContent(item).replace(/\s+/g, " ").trim();
816
+ if (typeof item.checked === "boolean") return `${item.checked ? "[x]" : "[ ]"} ${text}`.trim();
817
+ return text;
818
+ }
819
+ function tableBlock(node) {
820
+ const rows = (node.children ?? []).map(
821
+ (row) => (row.children ?? []).map((cell) => textContent(cell).replace(/\s+/g, " ").trim())
822
+ );
823
+ const [header = [], ...bodyRows] = rows;
824
+ return {
825
+ type: "table",
826
+ align: (node.align ?? []).map(toTableAlign),
827
+ header,
828
+ rows: bodyRows
829
+ };
830
+ }
831
+ function toTableAlign(value) {
832
+ return value === "left" || value === "center" || value === "right" ? value : void 0;
833
+ }
834
+ function soleImage(node) {
835
+ const meaningful = (node.children ?? []).filter((child) => child.type !== "text" || child.value?.trim());
836
+ return meaningful.length === 1 && meaningful[0].type === "image" ? meaningful[0] : void 0;
837
+ }
838
+ function isJsxOnlyParagraph(node) {
839
+ const children = node.children ?? [];
840
+ return children.length > 0 && children.every(
841
+ (child) => child.type === "mdxJsxTextElement" || child.type === "mdxJsxFlowElement" || child.type === "text" && child.value?.trim() === ""
842
+ );
843
+ }
844
+ function sourceForNode(node, source) {
845
+ const start = node.position?.start?.offset;
846
+ const end = node.position?.end?.offset;
847
+ return typeof start === "number" && typeof end === "number" ? source.slice(start, end) : void 0;
848
+ }
849
+ function isComponentName(name) {
850
+ return /^[A-Z]/.test(name);
851
+ }
852
+ function isUnsafeHtmlElement(name) {
853
+ return /^(script|style|iframe|object|embed)$/i.test(name);
854
+ }
855
+ function headingDepth(value) {
856
+ if (value === 1 || value === 2 || value === 3) return value;
857
+ return 3;
858
+ }
859
+ function stringAttr(value) {
860
+ return value && value.length > 0 ? value : void 0;
861
+ }
862
+
863
+ // src/generator/mdx/assets.ts
864
+ function rewriteAssetUrls(source, assets) {
865
+ let result = source;
866
+ for (const asset of assets) {
867
+ const assetPath = localAssetRelativePath(asset.sourcePath);
868
+ result = result.replaceAll(`./assets/${assetPath}`, asset.publicPath);
869
+ result = result.replace(new RegExp(`(?<!/)assets/${escapeRegExp(assetPath)}`, "g"), asset.publicPath);
870
+ }
871
+ return result;
872
+ }
873
+ function rewriteRelativeMdxImports(source, sourceDir, generatedDir) {
874
+ return source.replace(/(from\s+|import\s+)(["'])(\.[^"']+)\2/g, (_match, prefix, quote, specifier) => {
875
+ const target = normalizePath2(`${sourceDir}/${specifier}`);
876
+ return `${prefix}${quote}${toRelativeImportPath(generatedDir, target)}${quote}`;
877
+ });
878
+ }
879
+ function componentImportPath(outDir, sourcePath) {
880
+ if (!sourcePath) return void 0;
881
+ const source = sourcePath.replace(/\/index\.(tsx|ts|jsx|js)$/, "");
882
+ return toRelativeImportPath(outDir, source);
883
+ }
884
+ function dirname(path) {
885
+ const normalized = normalizePath2(path);
886
+ const index = normalized.lastIndexOf("/");
887
+ return index === -1 ? "." : normalized.slice(0, index);
888
+ }
889
+ function localAssetRelativePath(sourcePath) {
890
+ const marker = "/assets/";
891
+ const normalized = normalizePath2(sourcePath);
892
+ const markerIndex = normalized.indexOf(marker);
893
+ return markerIndex === -1 ? normalized.split("/").at(-1) ?? normalized : normalized.slice(markerIndex + marker.length);
894
+ }
895
+ function normalizePath2(path) {
896
+ return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
897
+ }
898
+ function toRelativeImportPath(fromDir, target) {
899
+ const fromParts = normalizePath2(fromDir).split("/").filter(Boolean);
900
+ const targetParts = normalizePath2(target).split("/").filter(Boolean);
901
+ while (fromParts.length > 0 && targetParts.length > 0 && fromParts[0] === targetParts[0]) {
902
+ fromParts.shift();
903
+ targetParts.shift();
904
+ }
905
+ const relative4 = [...fromParts.map(() => ".."), ...targetParts].join("/");
906
+ return relative4.startsWith(".") ? relative4 : `./${relative4 || "."}`;
907
+ }
908
+ function escapeRegExp(value) {
909
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
910
+ }
911
+
912
+ // src/generator/assets.ts
913
+ async function buildAssetRefs(slug, assetPaths, input) {
914
+ return Promise.all(
915
+ assetPaths.map(async (sourcePath) => ({
916
+ sourcePath,
917
+ publicPath: `${normalizeMountPath(input.mountPath ?? `/${input.root}`)}/${encodeURIComponent(slug)}/assets/${assetName(
918
+ sourcePath,
919
+ input.root,
920
+ slug
921
+ )}`,
922
+ type: "local",
923
+ contentType: contentTypeForPath(sourcePath),
924
+ body: input.readBinary ? await input.readBinary(sourcePath) : void 0
925
+ }))
926
+ );
927
+ }
928
+ function assetName(sourcePath, root, slug) {
929
+ const normalizedPath = normalizePath3(sourcePath);
930
+ const prefix = `${normalizePath3(root).replace(/\/$/, "")}/${slug}/assets/`;
931
+ const relative4 = normalizedPath.startsWith(prefix) ? normalizedPath.slice(prefix.length) : normalizedPath.split("/").at(-1) ?? normalizedPath;
932
+ return relative4.split("/").map(encodeURIComponent).join("/");
933
+ }
934
+ function normalizeMountPath(value) {
935
+ const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
936
+ return withLeadingSlash.replace(/\/$/, "");
937
+ }
938
+ function normalizePath3(path) {
939
+ return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
940
+ }
941
+
942
+ // src/generator/package-entry.ts
943
+ var DECKS_RUNTIME_ENTRY = "hono-decks";
944
+ var DECKS_ADVANCED_ENTRY = "hono-decks/advanced";
945
+
946
+ // src/generator/mdx-module-generator.ts
947
+ import { compile } from "@mdx-js/mdx";
948
+ import remarkDirective from "remark-directive";
949
+ import remarkGfm2 from "remark-gfm";
950
+
951
+ // src/generator/mdx/emit.ts
952
+ function emitModuleDecksRouter(input) {
953
+ const runtimeEntry = JSON.stringify(DECKS_RUNTIME_ENTRY);
954
+ const advancedEntry = JSON.stringify(DECKS_ADVANCED_ENTRY);
955
+ const slideImports = input.decks.flatMap(
956
+ (deck) => deck.slideModules.map(
957
+ (slide, index) => `import ${slideImportName(deck.deck.slug, index)} from ${JSON.stringify(slide.importPath)};`
958
+ )
959
+ ).join("\n");
960
+ const componentImports = input.decks.filter((deck) => deck.componentModulePath).map((deck) => `import * as ${componentImportName(deck.deck.slug)} from ${JSON.stringify(deck.componentModulePath)};`).join("\n");
961
+ return `// @ts-nocheck
962
+ import { configureDecks, defineDecks } from ${advancedEntry};
963
+ import type { ConfiguredDecks, DecksConfig } from ${runtimeEntry};
964
+ import type { Env } from "hono";
965
+ import { decksClientEntry } from "./client-entry";
966
+ ${slideImports}
967
+ ${componentImports}
968
+
969
+ function withClientComponentIds(module, clientIds) {
970
+ const registry = {};
971
+ for (const [name, value] of Object.entries(module)) {
972
+ const clientId = clientIds[name];
973
+ if (typeof value === "function") {
974
+ registry[name] = clientId ? { component: value, clientId } : value;
975
+ continue;
976
+ }
977
+ if (value && typeof value === "object" && "component" in value) {
978
+ registry[name] = clientId ? { ...value, clientId } : value;
979
+ }
980
+ }
981
+ return registry;
982
+ }
983
+
984
+ const generatedDecks = defineDecks({
985
+ clientEntryAsset: decksClientEntry,
986
+ decks: [
987
+ ${input.decks.map(emitDeckObject).join(",\n")}
988
+ ]
989
+ });
990
+
991
+ export function createDecks<E extends Env = any>(config: DecksConfig<E>): ConfiguredDecks<E> {
992
+ return configureDecks(definedDecksFor<E>(), config);
993
+ }
994
+
995
+ function definedDecksFor<E extends Env>() {
996
+ return generatedDecks;
997
+ }
998
+ `;
999
+ }
1000
+ function emitDeckObject(deck) {
1001
+ return ` {
1002
+ slug: ${JSON.stringify(deck.deck.slug)},
1003
+ sourcePath: ${JSON.stringify(deck.deck.sourcePath)},
1004
+ kind: ${JSON.stringify(deck.deck.kind)},
1005
+ meta: ${serializeValue(deck.deck.meta, 3)},
1006
+ ${deck.deck.themeStyle ? ` "themeStyle": ${serializeValue(deck.deck.themeStyle, 3)},
1007
+ ` : ""}${deck.deck.themeSourcePath ? ` "themeSourcePath": ${JSON.stringify(deck.deck.themeSourcePath)},
1008
+ ` : ""}
1009
+ assets: ${serializeValue(deck.deck.assets, 3)},
1010
+ componentRegistry: ${deck.componentModulePath ? `withClientComponentIds(${componentImportName(deck.deck.slug)}, ${serializeValue(deck.clientComponentIds ?? {}, 3)})` : "{}"},
1011
+ warnings: ${serializeValue(deck.deck.warnings, 3)},
1012
+ slides: [
1013
+ ${deck.deck.slides.map(
1014
+ (slide) => ` {
1015
+ index: ${slide.index},
1016
+ meta: ${serializeValue(slide.meta, 5)},
1017
+ html: "",
1018
+ components: [],
1019
+ notes: ${serializeValue(slide.notes, 5)},
1020
+ render: ${slideImportName(deck.deck.slug, slide.index)}
1021
+ }`
1022
+ ).join(",\n")}
1023
+ ]
1024
+ }`;
1025
+ }
1026
+ function slideImportName(slug, index) {
1027
+ return `Slide_${safeIdentifier(slug)}_${index}`;
1028
+ }
1029
+ function componentImportName(slug) {
1030
+ return `Components_${safeIdentifier(slug)}`;
1031
+ }
1032
+ function safeIdentifier(value) {
1033
+ return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1034
+ }
1035
+ function serializeValue(value, depth) {
1036
+ const indent = " ".repeat(depth);
1037
+ const nextIndent = " ".repeat(depth + 1);
1038
+ if (value === void 0) return "undefined";
1039
+ if (value instanceof Uint8Array) return `new Uint8Array([${[...value].join(", ")}])`;
1040
+ if (Array.isArray(value)) {
1041
+ if (value.length === 0) return "[]";
1042
+ return `[
1043
+ ${value.map((item) => `${nextIndent}${serializeValue(item, depth + 1)}`).join(",\n")}
1044
+ ${indent}]`;
1045
+ }
1046
+ if (typeof value === "object" && value !== null) {
1047
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0);
1048
+ if (entries.length === 0) return "{}";
1049
+ return `{
1050
+ ${entries.map(([key, item]) => `${nextIndent}${JSON.stringify(key)}: ${serializeValue(item, depth + 1)}`).join(",\n")}
1051
+ ${indent}}`;
1052
+ }
1053
+ return JSON.stringify(value);
1054
+ }
1055
+
1056
+ // src/generator/mdx/ogp.ts
1057
+ async function resolveLinkCardMetadataByUrl(source, resolveOgp) {
1058
+ const result = /* @__PURE__ */ new Map();
1059
+ if (!resolveOgp) return result;
1060
+ for (const url of collectLinkCardUrls(source)) {
1061
+ try {
1062
+ const metadata = await resolveOgp(url);
1063
+ if (metadata) result.set(url, metadata);
1064
+ } catch {
1065
+ }
1066
+ }
1067
+ return result;
1068
+ }
1069
+ function collectLinkCardUrls(source) {
1070
+ const urls = /* @__PURE__ */ new Set();
1071
+ const pattern = /@\[card\]\(([^)\s]+)\)/g;
1072
+ for (const match of source.matchAll(pattern)) {
1073
+ urls.add(match[1]);
1074
+ }
1075
+ return [...urls];
1076
+ }
1077
+
1078
+ // src/generator/mdx/syntax.ts
1079
+ import { codeToHtml } from "shiki";
1080
+ function remarkDeckSyntax(input = {}) {
1081
+ return () => (tree) => {
1082
+ transformDeckSyntaxChildren(tree, input);
1083
+ };
1084
+ }
1085
+ function remarkCodeHighlight() {
1086
+ return async (tree) => {
1087
+ await highlightMarkdownNode(tree);
1088
+ };
1089
+ }
1090
+ function transformDeckSyntaxChildren(node, input) {
1091
+ if (!Array.isArray(node.children)) return;
1092
+ const children = [];
1093
+ for (const child of node.children) {
1094
+ transformDeckSyntaxChildren(child, input);
1095
+ rejectRemovedFireAuthoring(child);
1096
+ children.push(
1097
+ fireAttributeNode(child) ?? zennEmbedNode(child, input) ?? plainUrlLinkNode(child) ?? fireDirectiveNode(child) ?? unknownDirectiveFallback(child) ?? child
1098
+ );
1099
+ }
1100
+ node.children = children;
1101
+ }
1102
+ function rejectRemovedFireAuthoring(node) {
1103
+ if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") return;
1104
+ if (!Array.isArray(node.attributes)) return;
1105
+ if (node.attributes.some((attribute) => attribute.name === "$fire")) {
1106
+ throw new Error('The "$fire" prop is not supported. Use fire or fire="effect" on a block-level custom component.');
1107
+ }
1108
+ if (node.name === "Fire" && node.attributes.some((attribute) => attribute.name === "order")) {
1109
+ throw new Error('The Fire "order" prop is not supported. Fires reveal in source order.');
1110
+ }
1111
+ if (node.name === "Fire") {
1112
+ const atAttribute = node.attributes.find((attribute) => attribute.name === "at");
1113
+ if (atAttribute) fireAtAttributeValue(atAttribute.value);
1114
+ }
1115
+ }
1116
+ function fireAttributeNode(node) {
1117
+ if (node.type !== "mdxJsxFlowElement" && node.type !== "mdxJsxTextElement") return void 0;
1118
+ if (!Array.isArray(node.attributes)) return void 0;
1119
+ const fireAttribute = node.attributes.find(
1120
+ (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "fire"
1121
+ );
1122
+ if (!fireAttribute) return void 0;
1123
+ const atAttribute = node.attributes.find(
1124
+ (attribute) => attribute.type === "mdxJsxAttribute" && attribute.name === "at"
1125
+ );
1126
+ if (node.name === "Fire") {
1127
+ throw new Error('The "fire" attribute is not supported on <Fire>. Use the effect prop instead.');
1128
+ }
1129
+ if (!node.name || !/^[A-Z]/.test(node.name)) {
1130
+ throw new Error('The "fire" attribute is only supported on custom components. Use :::fire for Markdown content.');
1131
+ }
1132
+ if (node.type !== "mdxJsxFlowElement") {
1133
+ throw new Error('The "fire" attribute is only supported on block-level custom components. Move the component to its own line.');
1134
+ }
1135
+ if (fireAttribute.value !== null && fireAttribute.value !== void 0 && typeof fireAttribute.value !== "string") {
1136
+ throw new Error('The "fire" attribute accepts no value or a static effect name such as fire="fade-up".');
1137
+ }
1138
+ const effect = typeof fireAttribute.value === "string" ? fireAttribute.value.trim() : "";
1139
+ const at = atAttribute ? fireAtAttributeValue(atAttribute.value) : void 0;
1140
+ node.attributes = node.attributes.filter((attribute) => attribute !== fireAttribute && attribute !== atAttribute);
1141
+ return mdxElement(
1142
+ "Fire",
1143
+ [
1144
+ ...effect ? [mdxAttribute("effect", effect)] : [],
1145
+ ...at ? [mdxAttribute("at", at)] : []
1146
+ ],
1147
+ [node]
1148
+ );
1149
+ }
1150
+ function fireAtAttributeValue(value) {
1151
+ if (typeof value === "string") {
1152
+ const at = value.trim();
1153
+ if (/^(?:\d+|[+-]\d+)$/.test(at)) return at;
1154
+ }
1155
+ throw new Error('The fire "at" attribute accepts a non-negative integer or a relative value such as "+1".');
1156
+ }
1157
+ function zennEmbedNode(node, input) {
1158
+ if (node.type !== "paragraph" || !Array.isArray(node.children) || node.children.length !== 2) return void 0;
1159
+ const [prefix, link] = node.children;
1160
+ if (prefix?.type !== "text" || String(prefix.value ?? "").trim() !== "@") return void 0;
1161
+ if (link?.type !== "link" || typeof link.url !== "string") return void 0;
1162
+ const name = collectMarkdownText(link).trim().toLowerCase();
1163
+ if (name === "youtube") {
1164
+ return mdxElement(
1165
+ "EmbedFrame",
1166
+ [
1167
+ mdxAttribute("provider", "youtube"),
1168
+ mdxAttribute("src", toYoutubeEmbedUrl(link.url)),
1169
+ mdxAttribute("fallbackHref", link.url),
1170
+ mdxAttribute("title", "YouTube embed example")
1171
+ ],
1172
+ [{ type: "text", value: "Open YouTube embed" }]
1173
+ );
1174
+ }
1175
+ if (name === "x") {
1176
+ return mdxElement("TweetEmbed", [mdxAttribute("href", link.url), mdxAttribute("label", "Open post on X")], []);
1177
+ }
1178
+ if (name === "card") {
1179
+ const metadata = input.linkCardMetadata?.get(link.url);
1180
+ return mdxElement(
1181
+ "LinkCard",
1182
+ [
1183
+ mdxAttribute("href", link.url),
1184
+ ...metadataAttributes(metadata)
1185
+ ],
1186
+ []
1187
+ );
1188
+ }
1189
+ if (name === "embed" || name === "iframe") {
1190
+ return mdxElement(
1191
+ "EmbedFrame",
1192
+ [mdxAttribute("src", link.url), mdxAttribute("title", "Embedded content")],
1193
+ [{ type: "text", value: "Open embed" }]
1194
+ );
1195
+ }
1196
+ return void 0;
1197
+ }
1198
+ function metadataAttributes(metadata) {
1199
+ if (!metadata) return [];
1200
+ return [
1201
+ ...metadata.title ? [mdxAttribute("title", metadata.title)] : [],
1202
+ ...metadata.description ? [mdxAttribute("description", metadata.description)] : [],
1203
+ ...metadata.image ? [mdxAttribute("image", metadata.image)] : [],
1204
+ ...metadata.siteName ? [mdxAttribute("siteName", metadata.siteName)] : []
1205
+ ];
1206
+ }
1207
+ function plainUrlLinkNode(node) {
1208
+ if (node.type !== "paragraph" || !Array.isArray(node.children) || node.children.length !== 1) return void 0;
1209
+ const [child] = node.children;
1210
+ if (child?.type !== "text" || typeof child.value !== "string") return void 0;
1211
+ const value = child.value.trim();
1212
+ if (!isHttpUrl(value)) return void 0;
1213
+ return {
1214
+ type: "paragraph",
1215
+ children: [
1216
+ {
1217
+ type: "link",
1218
+ url: value,
1219
+ children: [{ type: "text", value }]
1220
+ }
1221
+ ]
1222
+ };
1223
+ }
1224
+ function isHttpUrl(value) {
1225
+ try {
1226
+ const url = new URL(value);
1227
+ return url.protocol === "https:" || url.protocol === "http:";
1228
+ } catch {
1229
+ return false;
1230
+ }
1231
+ }
1232
+ function fireDirectiveNode(node) {
1233
+ if (node.type !== "containerDirective" || node.name !== "fire") return void 0;
1234
+ const attributes = directiveAttributes(node);
1235
+ if (attributes.order !== void 0) {
1236
+ throw new Error('The fire "order" attribute is not supported. Fires reveal in source order.');
1237
+ }
1238
+ if (attributes.each === void 0 && (attributes.depth !== void 0 || attributes.every !== void 0)) {
1239
+ throw new Error('The fire "depth" and "every" attributes require each="item".');
1240
+ }
1241
+ if (attributes.each !== void 0) return fireEachItemNode(node, attributes);
1242
+ const at = attributes.at !== void 0 ? fireAtAttributeValue(attributes.at) : void 0;
1243
+ const fireAttributes = [
1244
+ ...typeof attributes.effect === "string" ? [mdxAttribute("effect", attributes.effect)] : [],
1245
+ ...at ? [mdxAttribute("at", at)] : []
1246
+ ];
1247
+ return mdxElement("Fire", fireAttributes, node.children ?? []);
1248
+ }
1249
+ function fireEachItemNode(node, attributes) {
1250
+ if (attributes.each !== "item") {
1251
+ throw new Error('The fire "each" attribute only accepts "item".');
1252
+ }
1253
+ const children = node.children ?? [];
1254
+ const list = children.length === 1 && children[0]?.type === "list" ? children[0] : void 0;
1255
+ if (!list) {
1256
+ throw new Error('fire each="item" must contain exactly one Markdown list.');
1257
+ }
1258
+ const depth = positiveFireInteger(attributes.depth, "depth", 1);
1259
+ const every = positiveFireInteger(attributes.every, "every", 1);
1260
+ const at = attributes.at !== void 0 ? fireAtAttributeValue(attributes.at) : void 0;
1261
+ const effect = attributes.effect ? fireEffectToken(attributes.effect) : void 0;
1262
+ const items = fireListItems(list, depth);
1263
+ for (const [itemIndex, item] of items.entries()) {
1264
+ const itemAt = fireListItemAt(at, every, itemIndex);
1265
+ item.data = {
1266
+ ...item.data,
1267
+ hProperties: {
1268
+ ...item.data?.hProperties,
1269
+ "data-hono-decks-fire": "true",
1270
+ ...itemAt ? { "data-fire-at": itemAt } : {},
1271
+ ...effect ? { "data-fire-effect": effect } : {}
1272
+ }
1273
+ };
1274
+ }
1275
+ return list;
1276
+ }
1277
+ function positiveFireInteger(value, name, fallback) {
1278
+ if (value === void 0) return fallback;
1279
+ const parsed = Number(value);
1280
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
1281
+ throw new Error(`The fire "${name}" attribute accepts a positive integer.`);
1282
+ }
1283
+ function fireListItems(list, maxDepth, depth = 1) {
1284
+ const items = [];
1285
+ for (const item of list.children ?? []) {
1286
+ if (item.type !== "listItem") continue;
1287
+ items.push(item);
1288
+ if (depth >= maxDepth) continue;
1289
+ for (const child of item.children ?? []) {
1290
+ if (child.type === "list") items.push(...fireListItems(child, maxDepth, depth + 1));
1291
+ }
1292
+ }
1293
+ return items;
1294
+ }
1295
+ function fireListItemAt(at, every, itemIndex) {
1296
+ if (at && /^\d+$/.test(at)) return String(Number(at) + Math.floor(itemIndex / every));
1297
+ if (!at && every === 1) return void 0;
1298
+ if (itemIndex === 0) return at;
1299
+ return itemIndex % every === 0 ? "+1" : "+0";
1300
+ }
1301
+ function fireEffectToken(value) {
1302
+ return value.trim().replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "fade";
1303
+ }
1304
+ function unknownDirectiveFallback(node) {
1305
+ if (node.type === "textDirective") return { type: "text", value: `:${node.name ?? ""}` };
1306
+ if (node.type === "leafDirective") return { type: "text", value: `::${node.name ?? ""}` };
1307
+ if (node.type === "containerDirective") {
1308
+ return {
1309
+ type: "paragraph",
1310
+ children: [{ type: "text", value: `:::${node.name ?? ""}` }, ...node.children ?? []]
1311
+ };
1312
+ }
1313
+ return void 0;
1314
+ }
1315
+ function directiveAttributes(node) {
1316
+ if (!node.attributes || Array.isArray(node.attributes)) return {};
1317
+ const result = {};
1318
+ for (const [key, value] of Object.entries(node.attributes)) {
1319
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1320
+ result[key] = String(value);
1321
+ }
1322
+ }
1323
+ return result;
1324
+ }
1325
+ function mdxElement(name, attributes, children) {
1326
+ return {
1327
+ type: "mdxJsxFlowElement",
1328
+ name,
1329
+ attributes,
1330
+ children
1331
+ };
1332
+ }
1333
+ function toYoutubeEmbedUrl(value) {
1334
+ try {
1335
+ const url = new URL(value);
1336
+ const host = url.hostname.replace(/^www\./, "");
1337
+ if (host === "youtu.be") {
1338
+ const id = url.pathname.split("/").filter(Boolean)[0];
1339
+ return id ? `https://www.youtube.com/embed/${id}` : value;
1340
+ }
1341
+ if (host === "youtube.com" || host === "m.youtube.com") {
1342
+ if (url.pathname.startsWith("/embed/")) return value;
1343
+ const id = url.searchParams.get("v");
1344
+ return id ? `https://www.youtube.com/embed/${id}` : value;
1345
+ }
1346
+ return value;
1347
+ } catch {
1348
+ return value;
1349
+ }
1350
+ }
1351
+ async function highlightMarkdownNode(node) {
1352
+ if (node.type === "code") {
1353
+ const code = typeof node.value === "string" ? node.value : "";
1354
+ const lang = typeof node.lang === "string" && node.lang ? node.lang : void 0;
1355
+ const highlightedHtml = await highlightCodeBlock(code, lang);
1356
+ node.type = "mdxJsxFlowElement";
1357
+ node.name = "CodeBlock";
1358
+ node.attributes = [
1359
+ ...lang ? [mdxAttribute("lang", lang)] : [],
1360
+ mdxAttribute("highlightedHtml", highlightedHtml)
1361
+ ];
1362
+ node.children = [{ type: "text", value: code }];
1363
+ delete node.value;
1364
+ delete node.lang;
1365
+ delete node.meta;
1366
+ return;
1367
+ }
1368
+ if (node.type === "mdxJsxFlowElement" && node.name === "CodeBlock") {
1369
+ const code = collectMarkdownText(node);
1370
+ if (code.trim()) {
1371
+ const lang = getMdxStringAttribute(node, "lang");
1372
+ const highlightedHtml = await highlightCodeBlock(code, lang);
1373
+ node.attributes = upsertMdxStringAttribute(node.attributes, "highlightedHtml", highlightedHtml);
1374
+ }
1375
+ return;
1376
+ }
1377
+ if (!Array.isArray(node.children)) return;
1378
+ await Promise.all(node.children.map((child) => highlightMarkdownNode(child)));
1379
+ }
1380
+ async function highlightCodeBlock(code, lang) {
1381
+ const language = lang && /^[A-Za-z0-9_#+.-]+$/.test(lang) ? lang : "text";
1382
+ try {
1383
+ return await codeToHtml(code, { lang: language, theme: "github-dark" });
1384
+ } catch (error) {
1385
+ if (language === "text") throw error;
1386
+ return codeToHtml(code, { lang: "text", theme: "github-dark" });
1387
+ }
1388
+ }
1389
+ function mdxAttribute(name, value) {
1390
+ return { type: "mdxJsxAttribute", name, value };
1391
+ }
1392
+ function getMdxStringAttribute(node, name) {
1393
+ const attributes = Array.isArray(node.attributes) ? node.attributes : [];
1394
+ const attribute = attributes.find((item) => item.type === "mdxJsxAttribute" && item.name === name);
1395
+ return typeof attribute?.value === "string" ? attribute.value : void 0;
1396
+ }
1397
+ function upsertMdxStringAttribute(attributes, name, value) {
1398
+ const next = Array.isArray(attributes) ? [...attributes] : [];
1399
+ const index = next.findIndex((item) => item.type === "mdxJsxAttribute" && item.name === name);
1400
+ const attribute = mdxAttribute(name, value);
1401
+ if (index === -1) return [...next, attribute];
1402
+ next[index] = attribute;
1403
+ return next;
1404
+ }
1405
+ function collectMarkdownText(node) {
1406
+ if (typeof node.value === "string") return node.value;
1407
+ if (!Array.isArray(node.children)) return "";
1408
+ return node.children.map((child) => collectMarkdownText(child)).join(node.type === "paragraph" ? "\n" : "");
1409
+ }
1410
+
1411
+ // src/generator/mdx-module-generator.ts
1412
+ async function compileMdxModuleDecks(input) {
1413
+ const decks = await Promise.all(input.decks.map((entry) => compileMdxModuleDeck(input, entry)));
1414
+ return {
1415
+ decks,
1416
+ routerModule: emitModuleDecksRouter({ decks })
1417
+ };
1418
+ }
1419
+ async function compileMdxModuleDeck(input, entry) {
1420
+ const source = await input.readText(entry.sourcePath);
1421
+ const { attrs, body } = readFrontmatter(source);
1422
+ const { prelude, body: contentBody } = extractDeckPrelude(body);
1423
+ const slideSources = splitSlideSources(contentBody);
1424
+ let warnings = [];
1425
+ const deckMeta = toDeckFrontmatter(attrs, warnings);
1426
+ addUnknownFrontmatterWarnings(warnings, deckMeta.meta, "deck");
1427
+ const assets = [
1428
+ ...await buildAssetRefs(entry.slug, entry.assetPaths, input),
1429
+ ...collectGeneratedExternalAssetRefs(contentBody, deckMeta)
1430
+ ];
1431
+ const componentModulePath = componentImportPath(input.outDir, input.componentModulePaths?.[entry.slug]);
1432
+ const themeStyle = entry.kind === "directory" ? input.themeStyles?.[entry.slug] : void 0;
1433
+ const slideModules = [];
1434
+ const slides = [];
1435
+ for (let index = 0; index < slideSources.length; index += 1) {
1436
+ const { attrs: slideAttrs, body: slideBody } = readFrontmatter(slideSources[index]);
1437
+ const speakerNotes = extractMdxCommentSpeakerNotes(slideBody);
1438
+ const parsed = parseDeckWithWarnings(speakerNotes.body);
1439
+ warnings = warnings.concat(toGeneratedParserCompileWarnings(parsed.warnings, index));
1440
+ const firstParsedSlide = parsed.slides[0];
1441
+ const slideModulePath = `${input.outDir}/decks/${entry.slug}/slide-${index}.ts`;
1442
+ const slideMeta = toSlideFrontmatter(slideAttrs, warnings, {
1443
+ slideIndex: index,
1444
+ fallbackTransition: deckMeta.transition,
1445
+ fallbackTransitionDuration: deckMeta.transitionDuration,
1446
+ fallbackTransitionEasing: deckMeta.transitionEasing,
1447
+ fallbackTitle: firstParsedSlide?.title,
1448
+ fallbackLayout: firstParsedSlide?.layout,
1449
+ fallbackClassName: firstParsedSlide?.className
1450
+ });
1451
+ addUnknownFrontmatterWarnings(warnings, slideMeta.meta, "slide", index);
1452
+ const moduleSource = [prelude, rewriteAssetUrls(speakerNotes.body, assets)].filter(Boolean).join("\n\n");
1453
+ const rewrittenSource = rewriteRelativeMdxImports(moduleSource, dirname(entry.sourcePath), dirname(slideModulePath));
1454
+ const code = await compileMdxModule(rewrittenSource, entry.sourcePath, index, input.resolveOgp);
1455
+ slideModules.push({
1456
+ path: slideModulePath,
1457
+ importPath: `./decks/${entry.slug}/slide-${index}`,
1458
+ code
1459
+ });
1460
+ slides.push({
1461
+ index,
1462
+ meta: slideMeta,
1463
+ html: "",
1464
+ components: [],
1465
+ notes: combineSpeakerNotes(slideMeta.notes, speakerNotes.notes)
1466
+ });
1467
+ }
1468
+ addExternalAssetWarnings(warnings, assets);
1469
+ return {
1470
+ componentModulePath,
1471
+ clientComponentIds: input.clientComponentIds?.[entry.slug],
1472
+ deck: {
1473
+ slug: entry.slug,
1474
+ sourcePath: entry.sourcePath,
1475
+ kind: entry.kind,
1476
+ meta: deckMeta,
1477
+ themeStyle: themeStyle?.style,
1478
+ themeSourcePath: themeStyle?.sourcePath,
1479
+ slides,
1480
+ assets,
1481
+ warnings
1482
+ },
1483
+ slideModules
1484
+ };
1485
+ }
1486
+ function collectGeneratedExternalAssetRefs(contentBody, deckMeta) {
1487
+ return buildExternalAssetRefs([
1488
+ ...collectMarkdownAssetCandidates(contentBody),
1489
+ ...collectFrontmatterAssetCandidates(deckMeta.assets)
1490
+ ]);
1491
+ }
1492
+ function generatedMdxParserWarnings(warnings) {
1493
+ return warnings.filter((warning) => warning.code === "code-fence-unclosed");
1494
+ }
1495
+ function toGeneratedParserCompileWarnings(warnings, slideIndex) {
1496
+ return generatedMdxParserWarnings(warnings).map((warning) => ({
1497
+ code: "parse-warning",
1498
+ message: warning.message,
1499
+ slideIndex
1500
+ }));
1501
+ }
1502
+ async function compileMdxModule(source, sourcePath, slideIndex, resolveOgp) {
1503
+ try {
1504
+ const linkCardMetadata = await resolveLinkCardMetadataByUrl(source, resolveOgp);
1505
+ const compiled = String(
1506
+ await compile(
1507
+ { path: `${sourcePath}#slide-${slideIndex + 1}`, value: source },
1508
+ {
1509
+ jsxRuntime: "automatic",
1510
+ jsxImportSource: "hono/jsx",
1511
+ format: "mdx",
1512
+ elementAttributeNameCase: "html",
1513
+ remarkPlugins: [
1514
+ remarkGfm2,
1515
+ remarkDirective,
1516
+ remarkDeckSyntax({ linkCardMetadata }),
1517
+ remarkCodeHighlight
1518
+ ]
1519
+ }
1520
+ )
1521
+ );
1522
+ return `// @ts-nocheck
1523
+ ${compiled}`;
1524
+ } catch (error) {
1525
+ const message = error instanceof Error ? error.message : String(error);
1526
+ throw new CompileError(
1527
+ `MDX compile failed in ${sourcePath} slide ${slideIndex + 1}: ${message}`,
1528
+ "mdx-compile-error"
1529
+ );
1530
+ }
1531
+ }
1532
+ function extractDeckPrelude(source) {
1533
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
1534
+ const prelude = [];
1535
+ let cursor = 0;
1536
+ while (cursor < lines.length) {
1537
+ const line = lines[cursor];
1538
+ if (line.trim() === "") {
1539
+ prelude.push(line);
1540
+ cursor += 1;
1541
+ continue;
1542
+ }
1543
+ if (/^\s*(import|export)\s/.test(line)) {
1544
+ prelude.push(line);
1545
+ cursor += 1;
1546
+ continue;
1547
+ }
1548
+ break;
1549
+ }
1550
+ return {
1551
+ prelude: prelude.join("\n").trim(),
1552
+ body: lines.slice(cursor).join("\n").trim()
1553
+ };
1554
+ }
1555
+
1556
+ // src/node/client-entry.ts
1557
+ import { existsSync } from "fs";
1558
+ import { readFile } from "fs/promises";
1559
+ import { join } from "path";
1560
+ import { fileURLToPath } from "url";
1561
+ import { build as buildBrowserBundle } from "esbuild";
1562
+ async function discoverClientComponentIds(input) {
1563
+ const result = {};
1564
+ for (const entry of input.clientEntries) {
1565
+ const source = await readFile(join(input.cwd, entry.sourcePath), "utf8");
1566
+ const exports = extractComponentExportNames(source);
1567
+ result[entry.slug] = Object.fromEntries(exports.map((name) => [name, clientComponentId(entry.slug, name)]));
1568
+ }
1569
+ return result;
1570
+ }
1571
+ async function emitClientEntryModule(input) {
1572
+ const imports = [];
1573
+ const registrations = [];
1574
+ for (const entry of input.clientEntries) {
1575
+ const ids = input.clientComponentIds[entry.slug] ?? {};
1576
+ for (const [exportName, clientId] of Object.entries(ids)) {
1577
+ const localName = clientImportName(entry.slug, exportName);
1578
+ imports.push(`import { ${exportName} as ${localName} } from ${JSON.stringify(join(input.cwd, entry.sourcePath))};`);
1579
+ registrations.push(`${JSON.stringify(clientId)}: ${localName}`);
1580
+ }
1581
+ }
1582
+ if (registrations.length === 0) return 'export const decksClientEntry = "";\n';
1583
+ const entryContents = `import { hydrateSlideIslands } from "hono-decks/client";
1584
+ ${imports.join("\n")}
1585
+
1586
+ hydrateSlideIslands({
1587
+ components: {
1588
+ ${registrations.join(",\n ")}
1589
+ }
1590
+ });
1591
+ `;
1592
+ const result = await buildBrowserBundle({
1593
+ stdin: {
1594
+ contents: entryContents,
1595
+ resolveDir: input.cwd,
1596
+ sourcefile: "hono-decks-client-entry.tsx",
1597
+ loader: "tsx"
1598
+ },
1599
+ bundle: true,
1600
+ write: false,
1601
+ format: "esm",
1602
+ platform: "browser",
1603
+ target: "es2022",
1604
+ jsx: "automatic",
1605
+ jsxImportSource: "hono/jsx/dom",
1606
+ nodePaths: nodeModuleFallbackPaths(input.cwd),
1607
+ alias: { "hono-decks/client": resolveClientRuntimeEntry() },
1608
+ sourcemap: false,
1609
+ minify: false
1610
+ });
1611
+ const output = result.outputFiles[0];
1612
+ if (!output) throw new Error("Client entry did not produce output.");
1613
+ return `export const decksClientEntry = ${JSON.stringify(output.text)};
1614
+ `;
1615
+ }
1616
+ function resolveClientRuntimeEntry() {
1617
+ const built = fileURLToPath(new URL("./client.js", import.meta.url));
1618
+ if (existsSync(built)) return built;
1619
+ return fileURLToPath(new URL("../client.ts", import.meta.url));
1620
+ }
1621
+ function extractComponentExportNames(source) {
1622
+ const names = /* @__PURE__ */ new Set();
1623
+ for (const match of source.matchAll(/\bexport\s+function\s+([A-Z][A-Za-z0-9_]*)\b/g)) {
1624
+ names.add(match[1]);
1625
+ }
1626
+ for (const match of source.matchAll(/\bexport\s+const\s+([A-Z][A-Za-z0-9_]*)\b/g)) {
1627
+ names.add(match[1]);
1628
+ }
1629
+ return [...names].sort();
1630
+ }
1631
+ function nodeModuleFallbackPaths(cwd) {
1632
+ const current = process.cwd();
1633
+ return [
1634
+ join(cwd, "node_modules"),
1635
+ join(cwd, "..", "node_modules"),
1636
+ join(cwd, "..", "..", "node_modules"),
1637
+ join(current, "node_modules"),
1638
+ join(current, "..", "node_modules"),
1639
+ join(current, "..", "..", "node_modules")
1640
+ ];
1641
+ }
1642
+ function clientComponentId(slug, exportName) {
1643
+ const base = `${exportName}__${safeIdentifier2(slug)}`;
1644
+ return `${base}_${hashString(`${slug}:${exportName}`).slice(0, 8)}`;
1645
+ }
1646
+ function clientImportName(slug, exportName) {
1647
+ return `${safeIdentifier2(exportName)}__${safeIdentifier2(slug)}_${hashString(`${slug}:${exportName}`).slice(0, 8)}`;
1648
+ }
1649
+ function hashString(value) {
1650
+ let hash = 5381;
1651
+ for (let index = 0; index < value.length; index += 1) {
1652
+ hash = hash * 33 ^ value.charCodeAt(index);
1653
+ }
1654
+ return (hash >>> 0).toString(36);
1655
+ }
1656
+ function safeIdentifier2(value) {
1657
+ return value.replace(/[^A-Za-z0-9_$]+/g, "_").replace(/^[^A-Za-z_$]+/, "_") || "_";
1658
+ }
1659
+
1660
+ // src/node/local-deck-io.ts
1661
+ import { existsSync as existsSync2, watch } from "fs";
1662
+ import { readdir, readFile as readFile2 } from "fs/promises";
1663
+ import { join as join2, relative as relative2 } from "path";
1664
+
1665
+ // src/node/path-utils.ts
1666
+ import { relative } from "path";
1667
+ function dirname2(path) {
1668
+ const normalized = normalizePath4(path);
1669
+ return normalized.includes("/") ? normalized.slice(0, normalized.lastIndexOf("/")) : ".";
1670
+ }
1671
+ function normalizePath4(path) {
1672
+ return path.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
1673
+ }
1674
+ function normalizeDeckRoot(root) {
1675
+ const normalized = normalizeRelativePath(root, "Deck root").replace(/\/$/, "");
1676
+ if (normalized === ".") {
1677
+ throw new Error("Deck root must be a relative path inside the current working directory");
1678
+ }
1679
+ return normalized;
1680
+ }
1681
+ function normalizeRelativePath(path, label) {
1682
+ const normalized = normalizePath4(path).replace(/\/$/, "");
1683
+ const segments = normalized.split("/");
1684
+ if (normalized === "" || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || segments.includes("..")) {
1685
+ throw new Error(`${label} must be a relative path inside the current working directory`);
1686
+ }
1687
+ return normalized;
1688
+ }
1689
+
1690
+ // src/node/local-deck-io.ts
1691
+ async function listFiles(cwd, dir) {
1692
+ const entries = await readdir(dir, { withFileTypes: true });
1693
+ const paths = await Promise.all(
1694
+ entries.map(async (entry) => {
1695
+ const fullPath = join2(dir, entry.name);
1696
+ if (entry.isDirectory()) return listFiles(cwd, fullPath);
1697
+ if (entry.isFile()) return [normalizePath4(relative2(cwd, fullPath))];
1698
+ return [];
1699
+ })
1700
+ );
1701
+ return paths.flat().sort();
1702
+ }
1703
+
1704
+ // src/node/ogp.ts
1705
+ import { lookup } from "dns/promises";
1706
+ import { isIP } from "net";
1707
+ var MAX_OGP_BYTES = 256 * 1024;
1708
+ var MAX_OGP_REDIRECTS = 3;
1709
+ async function resolveOgpMetadata(url) {
1710
+ if (!isHttpUrl2(url) || typeof fetch === "undefined") return void 0;
1711
+ try {
1712
+ const response = await fetchPublicHttpUrl(url);
1713
+ if (!response) return void 0;
1714
+ const contentType = response.headers.get("content-type") ?? "";
1715
+ if (!response.ok || !contentType.toLowerCase().includes("text/html") || isTooLarge(response)) return void 0;
1716
+ const html = await readTextWithLimit(response, MAX_OGP_BYTES);
1717
+ if (!html) return void 0;
1718
+ return parseOgpHtml(html, response.url || url);
1719
+ } catch {
1720
+ return void 0;
1721
+ }
1722
+ }
1723
+ async function fetchPublicHttpUrl(url) {
1724
+ let current = url;
1725
+ for (let redirect = 0; redirect <= MAX_OGP_REDIRECTS; redirect += 1) {
1726
+ if (!await isPublicHttpUrl(current)) return void 0;
1727
+ const response = await fetch(current, {
1728
+ headers: { accept: "text/html,application/xhtml+xml" },
1729
+ redirect: "manual",
1730
+ signal: AbortSignal.timeout(1500)
1731
+ });
1732
+ if (!isRedirect(response.status)) return response;
1733
+ await response.body?.cancel();
1734
+ const location = response.headers.get("location");
1735
+ if (!location) return void 0;
1736
+ current = new URL(location, current).toString();
1737
+ }
1738
+ return void 0;
1739
+ }
1740
+ async function isPublicHttpUrl(value) {
1741
+ let url;
1742
+ try {
1743
+ url = new URL(value);
1744
+ } catch {
1745
+ return false;
1746
+ }
1747
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
1748
+ if (isLocalHostname(url.hostname)) return false;
1749
+ if (isPrivateIpAddress(url.hostname)) return false;
1750
+ const addresses = await lookup(url.hostname, { all: true, verbatim: true });
1751
+ return addresses.length > 0 && addresses.every(({ address }) => !isPrivateIpAddress(address));
1752
+ }
1753
+ function isLocalHostname(hostname) {
1754
+ const normalized = hostname.toLowerCase().replace(/\.$/, "");
1755
+ return normalized === "localhost" || normalized.endsWith(".localhost");
1756
+ }
1757
+ function isPrivateIpAddress(value) {
1758
+ const normalized = value.startsWith("[") && value.endsWith("]") ? value.slice(1, -1) : value;
1759
+ const version = isIP(normalized);
1760
+ if (version === 4) return isPrivateIpv4(normalized);
1761
+ if (version === 6) return isPrivateIpv6(normalized);
1762
+ return false;
1763
+ }
1764
+ function isPrivateIpv4(address) {
1765
+ const parts = address.split(".").map((part) => Number.parseInt(part, 10));
1766
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return true;
1767
+ const [a, b] = parts;
1768
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224;
1769
+ }
1770
+ function isPrivateIpv6(address) {
1771
+ const normalized = address.toLowerCase();
1772
+ if (normalized === "::" || normalized === "::1" || normalized.startsWith("fe80:") || normalized.startsWith("fec0:") || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff")) {
1773
+ return true;
1774
+ }
1775
+ if (normalized.startsWith("::ffff:")) {
1776
+ const tail = normalized.slice("::ffff:".length);
1777
+ const dotted = tail.match(/^(?:0:)?(\d+\.\d+\.\d+\.\d+)$/);
1778
+ if (dotted) return isPrivateIpv4(dotted[1]);
1779
+ const hex = tail.match(/^(?:0:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
1780
+ if (hex) {
1781
+ const hi = Number.parseInt(hex[1], 16);
1782
+ const lo = Number.parseInt(hex[2], 16);
1783
+ const value32 = hi << 16 | lo;
1784
+ const ipv4 = [value32 >>> 24 & 255, value32 >>> 16 & 255, value32 >>> 8 & 255, value32 & 255].join(".");
1785
+ return isPrivateIpv4(ipv4);
1786
+ }
1787
+ }
1788
+ return false;
1789
+ }
1790
+ function isRedirect(status) {
1791
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
1792
+ }
1793
+ function isTooLarge(response) {
1794
+ const contentLength = response.headers.get("content-length");
1795
+ return contentLength != null && Number(contentLength) > MAX_OGP_BYTES;
1796
+ }
1797
+ async function readTextWithLimit(response, maxBytes) {
1798
+ const reader = response.body?.getReader();
1799
+ if (!reader) {
1800
+ const text = await response.text();
1801
+ return new TextEncoder().encode(text).byteLength > maxBytes ? void 0 : text;
1802
+ }
1803
+ const chunks = [];
1804
+ let bytes = 0;
1805
+ while (true) {
1806
+ const { done, value } = await reader.read();
1807
+ if (done) break;
1808
+ bytes += value.byteLength;
1809
+ if (bytes > maxBytes) {
1810
+ await reader.cancel();
1811
+ return void 0;
1812
+ }
1813
+ chunks.push(value);
1814
+ }
1815
+ const buffer = new Uint8Array(bytes);
1816
+ let offset = 0;
1817
+ for (const chunk of chunks) {
1818
+ buffer.set(chunk, offset);
1819
+ offset += chunk.byteLength;
1820
+ }
1821
+ return new TextDecoder().decode(buffer);
1822
+ }
1823
+ function parseOgpHtml(html, pageUrl) {
1824
+ const meta = collectHtmlMeta(html);
1825
+ const title = firstValue(meta, ["og:title", "twitter:title"]) ?? htmlTitle(html);
1826
+ const description = firstValue(meta, ["og:description", "twitter:description", "description"]);
1827
+ const image = absoluteUrl(firstValue(meta, ["og:image", "og:image:url", "twitter:image"]), pageUrl);
1828
+ const siteName = firstValue(meta, ["og:site_name", "application-name"]);
1829
+ const result = { title, description, image, siteName };
1830
+ return Object.values(result).some(Boolean) ? result : void 0;
1831
+ }
1832
+ function collectHtmlMeta(html) {
1833
+ const meta = /* @__PURE__ */ new Map();
1834
+ const tagPattern = /<meta\b[^>]*>/gi;
1835
+ for (const match of html.matchAll(tagPattern)) {
1836
+ const attributes = parseHtmlAttributes(match[0]);
1837
+ const key = attributes.property ?? attributes.name;
1838
+ const content = attributes.content;
1839
+ if (key && content && !meta.has(key.toLowerCase())) {
1840
+ meta.set(key.toLowerCase(), decodeHtmlEntities(content.trim()));
1841
+ }
1842
+ }
1843
+ return meta;
1844
+ }
1845
+ function parseHtmlAttributes(tag) {
1846
+ const attributes = {};
1847
+ const attributePattern = /([^\s"'<>/=]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
1848
+ for (const match of tag.matchAll(attributePattern)) {
1849
+ attributes[match[1].toLowerCase()] = match[2] ?? match[3] ?? match[4] ?? "";
1850
+ }
1851
+ return attributes;
1852
+ }
1853
+ function firstValue(meta, keys) {
1854
+ for (const key of keys) {
1855
+ const value = meta.get(key);
1856
+ if (value) return value;
1857
+ }
1858
+ return void 0;
1859
+ }
1860
+ function htmlTitle(html) {
1861
+ const match = html.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i);
1862
+ return match ? decodeHtmlEntities(match[1].replace(/\s+/g, " ").trim()) : void 0;
1863
+ }
1864
+ function absoluteUrl(value, base) {
1865
+ if (!value) return void 0;
1866
+ try {
1867
+ return new URL(value, base).toString();
1868
+ } catch {
1869
+ return value;
1870
+ }
1871
+ }
1872
+ function decodeHtmlEntities(value) {
1873
+ return value.replaceAll("&amp;", "&").replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1874
+ }
1875
+ function isHttpUrl2(value) {
1876
+ try {
1877
+ const url = new URL(value);
1878
+ return url.protocol === "http:" || url.protocol === "https:";
1879
+ } catch {
1880
+ return false;
1881
+ }
1882
+ }
1883
+
1884
+ // src/node/compile-decks.ts
1885
+ async function compileDecks(input) {
1886
+ const out = normalizeRelativePath(input.out, "Output directory");
1887
+ const root = normalizeDeckRoot(input.root);
1888
+ const paths = await listFiles(input.cwd, join3(input.cwd, root));
1889
+ const resolved = resolveDeckFiles(paths, root);
1890
+ const componentModulePaths = Object.fromEntries(
1891
+ resolved.flatMap((deck) => {
1892
+ const path = deck.kind === "directory" ? findComponentModule(paths, root, deck.slug) : void 0;
1893
+ return path ? [[deck.slug, path]] : [];
1894
+ })
1895
+ );
1896
+ const clientEntryPaths = resolved.flatMap((deck) => {
1897
+ const path = deck.kind === "directory" ? findClientEntryModule(paths, root, deck.slug) : void 0;
1898
+ return path ? [{ slug: deck.slug, sourcePath: path }] : [];
1899
+ });
1900
+ const themeStyles = await discoverDeckThemeStyles({ cwd: input.cwd, root, paths, decks: resolved });
1901
+ const clientComponentIds = await discoverClientComponentIds({
1902
+ cwd: input.cwd,
1903
+ clientEntries: clientEntryPaths
1904
+ });
1905
+ const ogpCache = await createOgpCacheResolver({
1906
+ cwd: input.cwd,
1907
+ cacheFile: input.ogpCacheFile,
1908
+ refresh: input.refreshOgp,
1909
+ resolveOgp: input.resolveOgp ?? resolveOgpMetadata
1910
+ });
1911
+ const generated = await compileMdxModuleDecks({
1912
+ root,
1913
+ outDir: out,
1914
+ mountPath: input.mountPath,
1915
+ decks: resolved,
1916
+ componentModulePaths,
1917
+ clientComponentIds,
1918
+ themeStyles,
1919
+ resolveOgp: ogpCache.resolveOgp,
1920
+ readText: (path) => readFile3(join3(input.cwd, path), "utf8"),
1921
+ readBinary: (path) => readFile3(join3(input.cwd, path))
1922
+ });
1923
+ for (const deck of generated.decks) {
1924
+ for (const slide of deck.slideModules) {
1925
+ await writeTextFile(join3(input.cwd, slide.path), slide.code);
1926
+ }
1927
+ }
1928
+ await writeTextFile(join3(input.cwd, out, "decks.ts"), generated.routerModule);
1929
+ await writeTextFile(
1930
+ join3(input.cwd, out, "client-entry.ts"),
1931
+ await emitClientEntryModule({ cwd: input.cwd, clientEntries: clientEntryPaths, clientComponentIds })
1932
+ );
1933
+ await ogpCache.write();
1934
+ return { decks: generated.decks.map((deck) => deck.deck) };
1935
+ }
1936
+ async function createOgpCacheResolver(input) {
1937
+ if (!input.cacheFile) {
1938
+ return {
1939
+ resolveOgp: input.resolveOgp,
1940
+ write: async () => void 0
1941
+ };
1942
+ }
1943
+ const cachePath = join3(input.cwd, normalizeRelativePath(input.cacheFile, "OGP cache file"));
1944
+ const cache = await readOgpCache(cachePath);
1945
+ let dirty = false;
1946
+ return {
1947
+ async resolveOgp(url) {
1948
+ if (!input.refresh && cache[url]) return cache[url];
1949
+ if (!input.refresh) return void 0;
1950
+ const metadata = await input.resolveOgp(url);
1951
+ if (metadata) {
1952
+ cache[url] = metadata;
1953
+ dirty = true;
1954
+ }
1955
+ return metadata;
1956
+ },
1957
+ async write() {
1958
+ if (!dirty) return;
1959
+ await writeTextFile(cachePath, `${JSON.stringify(cache, null, 2)}
1960
+ `);
1961
+ }
1962
+ };
1963
+ }
1964
+ async function readOgpCache(path) {
1965
+ let raw;
1966
+ try {
1967
+ raw = await readFile3(path, "utf8");
1968
+ } catch (error) {
1969
+ if (isNodeError(error) && error.code === "ENOENT") return {};
1970
+ throw error;
1971
+ }
1972
+ const parsed = JSON.parse(raw);
1973
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1974
+ throw new Error("OGP cache file must contain a JSON object");
1975
+ }
1976
+ const cache = {};
1977
+ for (const [url, value] of Object.entries(parsed)) {
1978
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
1979
+ const metadata = value;
1980
+ cache[url] = {
1981
+ ...typeof metadata.title === "string" ? { title: metadata.title } : {},
1982
+ ...typeof metadata.description === "string" ? { description: metadata.description } : {},
1983
+ ...typeof metadata.image === "string" ? { image: metadata.image } : {},
1984
+ ...typeof metadata.siteName === "string" ? { siteName: metadata.siteName } : {}
1985
+ };
1986
+ }
1987
+ return cache;
1988
+ }
1989
+ function isNodeError(error) {
1990
+ return error instanceof Error && "code" in error;
1991
+ }
1992
+ async function writeTextFile(path, contents) {
1993
+ await mkdir(dirname2(path), { recursive: true });
1994
+ await writeFile(path, contents, "utf8");
1995
+ }
1996
+ function findComponentModule(paths, root, slug) {
1997
+ const base = `${normalizePath4(root).replace(/\/$/, "")}/${slug}/components/index`;
1998
+ return paths.find((path) => {
1999
+ const normalized = normalizePath4(path);
2000
+ return normalized === `${base}.tsx` || normalized === `${base}.ts` || normalized === `${base}.jsx` || normalized === `${base}.js`;
2001
+ });
2002
+ }
2003
+ function findClientEntryModule(paths, root, slug) {
2004
+ const base = `${normalizePath4(root).replace(/\/$/, "")}/${slug}/components/client/index`;
2005
+ return paths.find((path) => {
2006
+ const normalized = normalizePath4(path);
2007
+ return normalized === `${base}.tsx` || normalized === `${base}.ts` || normalized === `${base}.jsx` || normalized === `${base}.js`;
2008
+ });
2009
+ }
2010
+ async function discoverDeckThemeStyles(input) {
2011
+ const result = {};
2012
+ for (const deck of input.decks) {
2013
+ if (deck.kind !== "directory") continue;
2014
+ const sourcePath = findDeckThemeStyle(input.paths, input.root, deck.slug);
2015
+ if (!sourcePath) continue;
2016
+ result[deck.slug] = {
2017
+ sourcePath,
2018
+ style: await readFile3(join3(input.cwd, sourcePath), "utf8")
2019
+ };
2020
+ }
2021
+ return result;
2022
+ }
2023
+ function findDeckThemeStyle(paths, root, slug) {
2024
+ const base = `${normalizePath4(root).replace(/\/$/, "")}/${slug}`;
2025
+ const themePath = `${base}/theme.css`;
2026
+ const stylesEntryPath = `${base}/styles/index.css`;
2027
+ const hasTheme = paths.includes(themePath);
2028
+ const hasStylesEntry = paths.includes(stylesEntryPath);
2029
+ if (hasTheme && hasStylesEntry) {
2030
+ throw new Error(`Deck ${slug} has both ${themePath} and ${stylesEntryPath}. Use only one theme CSS entry.`);
2031
+ }
2032
+ return hasTheme ? themePath : hasStylesEntry ? stylesEntryPath : void 0;
2033
+ }
2034
+
2035
+ // src/node/config.ts
2036
+ import { existsSync as existsSync3 } from "fs";
2037
+ import { resolve } from "path";
2038
+ import { fileURLToPath as fileURLToPath2 } from "url";
2039
+ import { build } from "esbuild";
2040
+ var DEFAULT_DECKS_CONFIG_FILE = "hono-decks.config.ts";
2041
+ async function loadDecksConfig(input) {
2042
+ const path = resolve(input.cwd, input.configFile ?? DEFAULT_DECKS_CONFIG_FILE);
2043
+ if (!existsSync3(path)) {
2044
+ throw new Error(`Config file not found: ${input.configFile ?? DEFAULT_DECKS_CONFIG_FILE}. Run \`hono-decks init\` first.`);
2045
+ }
2046
+ const result = await build({
2047
+ absWorkingDir: input.cwd,
2048
+ entryPoints: [path],
2049
+ bundle: true,
2050
+ format: "esm",
2051
+ platform: "node",
2052
+ target: "node20",
2053
+ write: false,
2054
+ logLevel: "silent",
2055
+ alias: {
2056
+ "hono-decks": resolveRuntimeEntry()
2057
+ }
2058
+ });
2059
+ const source = result.outputFiles[0]?.text;
2060
+ if (!source) throw new Error(`Could not load config file: ${path}`);
2061
+ const module = await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}#${Date.now()}`);
2062
+ const config = module.default;
2063
+ if (!config || typeof config !== "object") throw new Error(`Config must have a default export: ${path}`);
2064
+ if (typeof config.mountPath !== "string") throw new Error(`Config mountPath must be a string: ${path}`);
2065
+ return {
2066
+ path,
2067
+ config,
2068
+ root: config.build?.root ?? "decks",
2069
+ outDir: config.build?.outDir ?? "src/generated",
2070
+ ogpCacheFile: config.build?.ogpCacheFile
2071
+ };
2072
+ }
2073
+ function resolveRuntimeEntry() {
2074
+ const built = fileURLToPath2(new URL("./mod.js", import.meta.url));
2075
+ if (existsSync3(built)) return built;
2076
+ return fileURLToPath2(new URL("../mod.ts", import.meta.url));
2077
+ }
2078
+
2079
+ // src/vite.ts
2080
+ function honoDecks(options = {}) {
2081
+ let projectRoot = process.cwd();
2082
+ let configPath = resolve2(projectRoot, options.configFile ?? DEFAULT_DECKS_CONFIG_FILE);
2083
+ let deckRoot = resolve2(projectRoot, "decks");
2084
+ let resolvedConfig;
2085
+ let server;
2086
+ let watchedPaths = /* @__PURE__ */ new Set();
2087
+ let timer;
2088
+ let compiling = false;
2089
+ let queued = false;
2090
+ const syncWatchedPaths = async () => {
2091
+ if (!server) return;
2092
+ const next = /* @__PURE__ */ new Set([deckRoot, configPath]);
2093
+ const removed = [...watchedPaths].filter((path) => !next.has(path));
2094
+ if (removed.length > 0) await server.watcher.unwatch(removed);
2095
+ server.watcher.add([...next]);
2096
+ watchedPaths = next;
2097
+ };
2098
+ const compileOnce = async () => {
2099
+ const loaded = await loadDecksConfig({ cwd: projectRoot, configFile: options.configFile });
2100
+ const manifest = await compileDecks({
2101
+ cwd: projectRoot,
2102
+ root: loaded.root,
2103
+ out: loaded.outDir,
2104
+ mountPath: loaded.config.mountPath,
2105
+ ogpCacheFile: loaded.ogpCacheFile,
2106
+ refreshOgp: options.refreshOgp
2107
+ });
2108
+ configPath = loaded.path;
2109
+ deckRoot = resolve2(projectRoot, loaded.root);
2110
+ await syncWatchedPaths();
2111
+ resolvedConfig?.logger.info(`[hono-decks] Compiled ${manifest.decks.length} decks to ${loaded.outDir}`);
2112
+ };
2113
+ const compile2 = async (fatal) => {
2114
+ if (compiling) {
2115
+ queued = true;
2116
+ return;
2117
+ }
2118
+ compiling = true;
2119
+ let latestCompileSucceeded = false;
2120
+ try {
2121
+ do {
2122
+ queued = false;
2123
+ try {
2124
+ await compileOnce();
2125
+ latestCompileSucceeded = true;
2126
+ } catch (error) {
2127
+ latestCompileSucceeded = false;
2128
+ if (fatal) throw error;
2129
+ resolvedConfig?.logger.error(`[hono-decks] ${error instanceof Error ? error.message : String(error)}`);
2130
+ }
2131
+ } while (queued);
2132
+ if (latestCompileSucceeded && server) server.ws.send({ type: "full-reload" });
2133
+ } finally {
2134
+ compiling = false;
2135
+ }
2136
+ };
2137
+ const scheduleCompile = () => {
2138
+ if (timer) clearTimeout(timer);
2139
+ timer = setTimeout(() => void compile2(false), 75);
2140
+ };
2141
+ const onWatcherEvent = (event, path) => {
2142
+ if (event !== "add" && event !== "change" && event !== "unlink") return;
2143
+ const absolutePath = resolve2(projectRoot, path);
2144
+ if (absolutePath === configPath || isWithin(deckRoot, absolutePath)) scheduleCompile();
2145
+ };
2146
+ return {
2147
+ name: "hono-decks",
2148
+ enforce: "pre",
2149
+ async configResolved(config) {
2150
+ resolvedConfig = config;
2151
+ projectRoot = config.root;
2152
+ configPath = resolve2(projectRoot, options.configFile ?? DEFAULT_DECKS_CONFIG_FILE);
2153
+ await compile2(true);
2154
+ },
2155
+ configureServer(devServer) {
2156
+ server = devServer;
2157
+ void syncWatchedPaths();
2158
+ devServer.watcher.on("all", onWatcherEvent);
2159
+ const cleanup = () => {
2160
+ if (timer) clearTimeout(timer);
2161
+ devServer.watcher.off("all", onWatcherEvent);
2162
+ server = void 0;
2163
+ watchedPaths.clear();
2164
+ };
2165
+ devServer.httpServer?.once("close", cleanup);
2166
+ }
2167
+ };
2168
+ }
2169
+ function isWithin(root, path) {
2170
+ const child = relative3(root, path);
2171
+ return child === "" || child !== ".." && !child.startsWith("../") && !child.startsWith("..\\");
2172
+ }
2173
+ export {
2174
+ honoDecks
2175
+ };