@honeydeck/honeydeck 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/AGENTS.md +25 -0
- package/DEVELOPMENT.md +522 -0
- package/LICENSE +21 -0
- package/Readme.md +49 -0
- package/SPEC.md +88 -0
- package/docs/components.md +63 -0
- package/docs/configuration.md +91 -0
- package/docs/getting-started.md +116 -0
- package/docs/kit-authoring.md +207 -0
- package/docs/kits.md +387 -0
- package/docs/local-development.md +95 -0
- package/docs/mermaid.md +198 -0
- package/docs/mobile.md +108 -0
- package/docs/navigation.md +93 -0
- package/docs/next-steps.md +377 -0
- package/docs/pdf-export.md +91 -0
- package/docs/presenter-mode.md +104 -0
- package/docs/slides.md +130 -0
- package/docs/slidev-migration.md +42 -0
- package/docs/steps-and-reveals.md +171 -0
- package/package.json +134 -0
- package/skills/SPEC.md +21 -0
- package/skills/honeydeck/SKILL.md +65 -0
- package/skills/presentation-writing/SKILL.md +75 -0
- package/skills/slidev-migration/SKILL.md +153 -0
- package/src/SPEC.md +89 -0
- package/src/assets.d.ts +30 -0
- package/src/cli/SPEC.md +230 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/banner.ts +9 -0
- package/src/cli/bin.js +5 -0
- package/src/cli/build.ts +229 -0
- package/src/cli/deck-path.ts +32 -0
- package/src/cli/dev.ts +263 -0
- package/src/cli/index.ts +126 -0
- package/src/cli/init.ts +369 -0
- package/src/cli/pdf.ts +923 -0
- package/src/cli/skill.ts +75 -0
- package/src/cli/templates/SPEC.md +70 -0
- package/src/cli/templates/deck-mdx.ts +15 -0
- package/src/cli/templates/package-json.ts +36 -0
- package/src/cli/templates/sparkle-button.ts +15 -0
- package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
- package/src/cli/templates/starter/deck.mdx +153 -0
- package/src/cli/templates/starter/styles.css +14 -0
- package/src/cli/templates/styles-css.ts +14 -0
- package/src/defaults.ts +1 -0
- package/src/layouts/ColorModeImage.tsx +55 -0
- package/src/layouts/SPEC.md +393 -0
- package/src/layouts/SlideFrame.tsx +48 -0
- package/src/layouts/bee/Blank.tsx +12 -0
- package/src/layouts/bee/Cover.tsx +70 -0
- package/src/layouts/bee/Default.tsx +42 -0
- package/src/layouts/bee/Image/Image.tsx +151 -0
- package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
- package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
- package/src/layouts/bee/Image/placeholder.webp +0 -0
- package/src/layouts/bee/ImageLeft.tsx +27 -0
- package/src/layouts/bee/ImageRight.tsx +27 -0
- package/src/layouts/bee/ImageSide.tsx +107 -0
- package/src/layouts/bee/Section.tsx +40 -0
- package/src/layouts/bee/TwoCol.tsx +108 -0
- package/src/layouts/bee/index.ts +40 -0
- package/src/layouts/clean/Blank.tsx +12 -0
- package/src/layouts/clean/Cover.tsx +58 -0
- package/src/layouts/clean/Default.tsx +33 -0
- package/src/layouts/clean/Image/Image.tsx +103 -0
- package/src/layouts/clean/ImageLeft.tsx +27 -0
- package/src/layouts/clean/ImageRight.tsx +27 -0
- package/src/layouts/clean/ImageSide.tsx +113 -0
- package/src/layouts/clean/Section.tsx +35 -0
- package/src/layouts/clean/TwoCol.tsx +63 -0
- package/src/layouts/clean/index.ts +40 -0
- package/src/layouts/index.ts +60 -0
- package/src/layouts/placeholders.ts +9 -0
- package/src/layouts/utils.ts +13 -0
- package/src/remark/SPEC.md +49 -0
- package/src/remark/h1-extract.ts +124 -0
- package/src/remark/index.ts +4 -0
- package/src/remark/shiki-code-blocks.ts +325 -0
- package/src/remark/step-numbering.ts +412 -0
- package/src/runtime/Deck.tsx +533 -0
- package/src/runtime/SPEC.md +256 -0
- package/src/runtime/SlideCanvas.tsx +95 -0
- package/src/runtime/TimelineContext.tsx +122 -0
- package/src/runtime/app-shell/index.html +31 -0
- package/src/runtime/app-shell/main.tsx +42 -0
- package/src/runtime/aspectRatio.ts +34 -0
- package/src/runtime/colorMode.ts +23 -0
- package/src/runtime/components/BrowserFrame.tsx +233 -0
- package/src/runtime/components/Button.tsx +57 -0
- package/src/runtime/components/CodeBlock.tsx +210 -0
- package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
- package/src/runtime/components/ErrorBoundary.tsx +125 -0
- package/src/runtime/components/Keyboard.tsx +87 -0
- package/src/runtime/components/ListStyle.tsx +203 -0
- package/src/runtime/components/NavBar.tsx +223 -0
- package/src/runtime/components/NavBarButton.tsx +47 -0
- package/src/runtime/components/NavBarDivider.tsx +3 -0
- package/src/runtime/components/Notes.tsx +171 -0
- package/src/runtime/components/Reveal.tsx +82 -0
- package/src/runtime/components/RevealGroup.tsx +193 -0
- package/src/runtime/components/SPEC.md +263 -0
- package/src/runtime/components/SlideNumberBadge.tsx +11 -0
- package/src/runtime/components/TimelineSteps.tsx +115 -0
- package/src/runtime/components/index.ts +55 -0
- package/src/runtime/index.ts +42 -0
- package/src/runtime/inputOwnership.ts +68 -0
- package/src/runtime/keyboardTarget.ts +7 -0
- package/src/runtime/lastSlideRoute.ts +56 -0
- package/src/runtime/navigation.ts +211 -0
- package/src/runtime/router.ts +157 -0
- package/src/runtime/slideData.ts +137 -0
- package/src/runtime/sync.ts +267 -0
- package/src/runtime/types.ts +182 -0
- package/src/runtime/useKeyboardNav.ts +138 -0
- package/src/runtime/useSwipeNav.ts +257 -0
- package/src/runtime/views/DocsView.tsx +74 -0
- package/src/runtime/views/OverviewView.tsx +386 -0
- package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
- package/src/runtime/views/PresenterView.tsx +340 -0
- package/src/runtime/views/SPEC.md +152 -0
- package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
- package/src/runtime/views/docs/DocsHeader.tsx +101 -0
- package/src/runtime/views/docs/Intro.tsx +20 -0
- package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
- package/src/runtime/views/docs/ThemeTab.tsx +110 -0
- package/src/runtime/views/index.ts +7 -0
- package/src/runtime/views/overviewGrid.ts +106 -0
- package/src/runtime/views/presenterPreview.ts +27 -0
- package/src/runtime/virtual-modules.d.ts +98 -0
- package/src/theme/SPEC.md +179 -0
- package/src/theme/base.css +623 -0
- package/src/theme/bee.css +35 -0
- package/src/theme/clean.css +38 -0
- package/src/vite-plugin/SPEC.md +114 -0
- package/src/vite-plugin/component-doc-crawler.ts +350 -0
- package/src/vite-plugin/deck-loader.ts +148 -0
- package/src/vite-plugin/index.ts +373 -0
- package/src/vite-plugin/layout-demo-crawler.ts +802 -0
- package/src/vite-plugin/splitter.ts +353 -0
- package/src/vite-plugin/token-manifest.ts +163 -0
- package/src/vite-plugin/virtual-modules.ts +587 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual module resolution plugin for Honeydeck.
|
|
3
|
+
*
|
|
4
|
+
* Exposes virtual module families derived from a single `deck.mdx`:
|
|
5
|
+
*
|
|
6
|
+
* virtual:honeydeck/slide/N.mdx — standalone MDX source for slide N (0-based)
|
|
7
|
+
* virtual:honeydeck/slides — barrel re-export: `export { default as SlideN } from ...`
|
|
8
|
+
* virtual:honeydeck/config — deck-level frontmatter exported as a plain JS object
|
|
9
|
+
* virtual:honeydeck/layouts — active layout map + discovered demo metadata
|
|
10
|
+
* virtual:honeydeck/layout-demo/N.mdx — compiled MDX source for layout demo N
|
|
11
|
+
* virtual:honeydeck/components — built-in component docs metadata
|
|
12
|
+
*
|
|
13
|
+
* All virtual IDs are prefixed with `\0` (Vite convention) so they are
|
|
14
|
+
* invisible to other plugins that rely on file-system resolution.
|
|
15
|
+
*
|
|
16
|
+
* ### HMR strategy
|
|
17
|
+
* When `deck.mdx` changes, the plugin re-splits the file and compares the
|
|
18
|
+
* new segments against the cached ones. Only modules whose content actually
|
|
19
|
+
* changed are invalidated, enabling per-slide hot-reload instead of a
|
|
20
|
+
* blanket page refresh.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, statSync } from "node:fs";
|
|
24
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { compile } from "@mdx-js/mdx";
|
|
27
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
28
|
+
import remarkGfm from "remark-gfm";
|
|
29
|
+
import type { HmrContext, ModuleNode, Plugin, ViteDevServer } from "vite";
|
|
30
|
+
import { remarkH1Extract } from "../remark/h1-extract.ts";
|
|
31
|
+
import { remarkShikiCodeBlocks } from "../remark/shiki-code-blocks.ts";
|
|
32
|
+
import { remarkStepNumbering } from "../remark/step-numbering.ts";
|
|
33
|
+
import { crawlComponentDocs } from "./component-doc-crawler.ts";
|
|
34
|
+
import { type LoadedDeck, loadDeck } from "./deck-loader.ts";
|
|
35
|
+
import {
|
|
36
|
+
crawlLayoutDemos,
|
|
37
|
+
toFsImportSpecifier,
|
|
38
|
+
} from "./layout-demo-crawler.ts";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Virtual module ID constants
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/** Public import specifier prefix used in user/generated code. */
|
|
45
|
+
const VIRTUAL_SLIDE_PREFIX = "virtual:honeydeck/slide/";
|
|
46
|
+
const VIRTUAL_SLIDES_ID = "virtual:honeydeck/slides";
|
|
47
|
+
const VIRTUAL_CONFIG_ID = "virtual:honeydeck/config";
|
|
48
|
+
const VIRTUAL_LAYOUTS_ID = "virtual:honeydeck/layouts";
|
|
49
|
+
const VIRTUAL_LAYOUT_DEMO_PREFIX = "virtual:honeydeck/layout-demo/";
|
|
50
|
+
const VIRTUAL_COMPONENTS_ID = "virtual:honeydeck/components";
|
|
51
|
+
const VIRTUAL_COMPONENT_DOC_PREFIX = "virtual:honeydeck/component-doc/";
|
|
52
|
+
|
|
53
|
+
/** Resolved (internal) IDs — \0 prefix prevents accidental file-system hits. */
|
|
54
|
+
const RESOLVED_SLIDE_PREFIX = "\0virtual:honeydeck/slide/";
|
|
55
|
+
const RESOLVED_SLIDES_ID = "\0virtual:honeydeck/slides";
|
|
56
|
+
const RESOLVED_CONFIG_ID = "\0virtual:honeydeck/config";
|
|
57
|
+
const RESOLVED_LAYOUTS_ID = "\0virtual:honeydeck/layouts";
|
|
58
|
+
const RESOLVED_LAYOUT_DEMO_PREFIX = "\0virtual:honeydeck/layout-demo/";
|
|
59
|
+
const RESOLVED_COMPONENTS_ID = "\0virtual:honeydeck/components";
|
|
60
|
+
const RESOLVED_COMPONENT_DOC_PREFIX = "\0virtual:honeydeck/component-doc/";
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Public API
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export type VirtualModulesOptions = {
|
|
67
|
+
/** Absolute path to the entry deck MDX file (e.g. `/project/deck.mdx`). */
|
|
68
|
+
entryPath: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type LayoutDemoSource = {
|
|
72
|
+
layoutName: string;
|
|
73
|
+
modulePath: string;
|
|
74
|
+
source: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const EXTENSIONLESS_IMPORT_EXTENSIONS = [
|
|
78
|
+
".tsx",
|
|
79
|
+
".ts",
|
|
80
|
+
".jsx",
|
|
81
|
+
".js",
|
|
82
|
+
".mdx",
|
|
83
|
+
".md",
|
|
84
|
+
".css",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export function resolveRelativeImport(baseDir: string, id: string): string {
|
|
88
|
+
const absolute = resolve(baseDir, id);
|
|
89
|
+
|
|
90
|
+
if (extname(absolute)) return absolute;
|
|
91
|
+
|
|
92
|
+
if (existsSync(absolute) && statSync(absolute).isFile()) {
|
|
93
|
+
return absolute;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const extension of EXTENSIONLESS_IMPORT_EXTENSIONS) {
|
|
97
|
+
const candidate = `${absolute}${extension}`;
|
|
98
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
99
|
+
return candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const extension of EXTENSIONLESS_IMPORT_EXTENSIONS) {
|
|
104
|
+
const candidate = join(absolute, `index${extension}`);
|
|
105
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return absolute;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function virtualModulesPlugin(options: VirtualModulesOptions): Plugin {
|
|
114
|
+
const { entryPath } = options;
|
|
115
|
+
const entryFileName = basename(entryPath);
|
|
116
|
+
|
|
117
|
+
/** In-memory cache of the last split result. `null` until first read. */
|
|
118
|
+
let splitResult: LoadedDeck | null = null;
|
|
119
|
+
let devServer: ViteDevServer | null = null;
|
|
120
|
+
const watchedMdxFiles = new Set<string>();
|
|
121
|
+
const watchedLayoutFiles = new Set<string>();
|
|
122
|
+
const watchedComponentFiles = new Set<string>();
|
|
123
|
+
let layoutDemoSources: LayoutDemoSource[] = [];
|
|
124
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
125
|
+
|
|
126
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Read `entryPath` and split it. Populates / replaces `splitResult`. */
|
|
129
|
+
function loadAndSplit(): LoadedDeck {
|
|
130
|
+
const result = loadDeck(entryPath);
|
|
131
|
+
watchedMdxFiles.clear();
|
|
132
|
+
for (const file of result.watchedFiles) {
|
|
133
|
+
watchedMdxFiles.add(file);
|
|
134
|
+
devServer?.watcher.add(file);
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Return the cached split result, loading it on first call.
|
|
141
|
+
* Lazy to avoid race conditions between `buildStart` and `configureServer`
|
|
142
|
+
* across different Vite lifecycle orderings.
|
|
143
|
+
*/
|
|
144
|
+
function getResult(): LoadedDeck {
|
|
145
|
+
if (!splitResult) {
|
|
146
|
+
splitResult = loadAndSplit();
|
|
147
|
+
}
|
|
148
|
+
return splitResult;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Plugin definition ────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: "honeydeck:virtual-modules",
|
|
155
|
+
|
|
156
|
+
// Make sure Vite's chokidar watcher covers the entry file even when it is
|
|
157
|
+
// not imported as a real module (it may live outside the default watch
|
|
158
|
+
// directories in non-standard project layouts).
|
|
159
|
+
configureServer(server) {
|
|
160
|
+
devServer = server;
|
|
161
|
+
server.watcher.add(entryPath);
|
|
162
|
+
for (const file of getResult().watchedFiles) {
|
|
163
|
+
server.watcher.add(file);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ── resolveId ───────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
resolveId(id, importer) {
|
|
170
|
+
// Resolve relative imports from virtual slide modules against the entry
|
|
171
|
+
// file's directory. Without this, './styles.css' inside a compiled virtual
|
|
172
|
+
// slide has no filesystem anchor and Vite throws a resolution error.
|
|
173
|
+
if (
|
|
174
|
+
importer?.startsWith(RESOLVED_SLIDE_PREFIX) &&
|
|
175
|
+
(id.startsWith("./") || id.startsWith("../"))
|
|
176
|
+
) {
|
|
177
|
+
return resolveRelativeImport(dirname(entryPath), id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
importer === RESOLVED_LAYOUTS_ID &&
|
|
182
|
+
(id.startsWith("./") || id.startsWith("../"))
|
|
183
|
+
) {
|
|
184
|
+
return resolveRelativeImport(dirname(entryPath), id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
importer?.startsWith(RESOLVED_LAYOUT_DEMO_PREFIX) &&
|
|
189
|
+
(id.startsWith("./") || id.startsWith("../"))
|
|
190
|
+
) {
|
|
191
|
+
const suffix = importer.slice(RESOLVED_LAYOUT_DEMO_PREFIX.length);
|
|
192
|
+
const index = parseInt(suffix.replace(".mdx", ""), 10);
|
|
193
|
+
const demo = layoutDemoSources[index];
|
|
194
|
+
if (demo) return resolveRelativeImport(dirname(demo.modulePath), id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (id === VIRTUAL_SLIDES_ID) return RESOLVED_SLIDES_ID;
|
|
198
|
+
if (id === VIRTUAL_CONFIG_ID) return RESOLVED_CONFIG_ID;
|
|
199
|
+
if (id === VIRTUAL_LAYOUTS_ID) return RESOLVED_LAYOUTS_ID;
|
|
200
|
+
if (id === VIRTUAL_COMPONENTS_ID) return RESOLVED_COMPONENTS_ID;
|
|
201
|
+
// Matches `virtual:honeydeck/slide/0.mdx`, `virtual:honeydeck/slide/1.mdx`, …
|
|
202
|
+
if (id.startsWith(VIRTUAL_SLIDE_PREFIX)) return `\0${id}`;
|
|
203
|
+
if (id.startsWith(VIRTUAL_LAYOUT_DEMO_PREFIX)) return `\0${id}`;
|
|
204
|
+
if (id.startsWith(VIRTUAL_COMPONENT_DOC_PREFIX)) return `\0${id}`;
|
|
205
|
+
return null;
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// ── load ────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
async load(id) {
|
|
211
|
+
// ── virtual:honeydeck/config ─────────────────────────────────────────────
|
|
212
|
+
if (id === RESOLVED_CONFIG_ID) {
|
|
213
|
+
const { deckFrontmatter } = getResult();
|
|
214
|
+
return [
|
|
215
|
+
`export const config = ${JSON.stringify(deckFrontmatter, null, 2)};`,
|
|
216
|
+
`export default config;`,
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── virtual:honeydeck/layouts ────────────────────────────────────────────
|
|
221
|
+
if (id === RESOLVED_LAYOUTS_ID) {
|
|
222
|
+
const { deckFrontmatter } = getResult();
|
|
223
|
+
const layoutSpecifier =
|
|
224
|
+
typeof deckFrontmatter.layouts === "string"
|
|
225
|
+
? deckFrontmatter.layouts
|
|
226
|
+
: "";
|
|
227
|
+
const crawl = crawlLayoutDemos({
|
|
228
|
+
entryPath,
|
|
229
|
+
packageRoot,
|
|
230
|
+
layoutSpecifier,
|
|
231
|
+
});
|
|
232
|
+
const runtimeSpecifier =
|
|
233
|
+
layoutSpecifier.trim().startsWith(".") && crawl.mapPath
|
|
234
|
+
? toFsImportSpecifier(crawl.mapPath)
|
|
235
|
+
: layoutSpecifier.trim() || "@honeydeck/honeydeck/layouts";
|
|
236
|
+
|
|
237
|
+
watchedLayoutFiles.clear();
|
|
238
|
+
for (const file of crawl.watchedFiles) {
|
|
239
|
+
watchedLayoutFiles.add(file);
|
|
240
|
+
this.addWatchFile(file);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const importLines: string[] = [
|
|
244
|
+
`import layoutMap from ${JSON.stringify(runtimeSpecifier)};`,
|
|
245
|
+
];
|
|
246
|
+
const demoEntries: string[] = [];
|
|
247
|
+
const propDocEntries: string[] = [];
|
|
248
|
+
layoutDemoSources = [];
|
|
249
|
+
|
|
250
|
+
crawl.demos.forEach((demo, index) => {
|
|
251
|
+
propDocEntries.push(
|
|
252
|
+
`${JSON.stringify(demo.layoutName)}: ${JSON.stringify(demo.propDocs)}`,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const mdx = demo.demoMetadata?.mdx;
|
|
256
|
+
if (typeof mdx !== "string" || mdx.length === 0) {
|
|
257
|
+
if (demo.demoMetadata?.dynamicMdx) {
|
|
258
|
+
crawl.warnings.push(
|
|
259
|
+
`Layout "${demo.layoutName}" demo.mdx is dynamic; live demo rendering skipped.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const demoIndex =
|
|
266
|
+
layoutDemoSources.push({
|
|
267
|
+
layoutName: demo.layoutName,
|
|
268
|
+
modulePath: demo.modulePath,
|
|
269
|
+
source: mdx,
|
|
270
|
+
}) - 1;
|
|
271
|
+
const moduleId = `${VIRTUAL_LAYOUT_DEMO_PREFIX}${demoIndex}.mdx`;
|
|
272
|
+
const componentName = `LayoutDemo${index}`;
|
|
273
|
+
importLines.push(
|
|
274
|
+
`import ${componentName}, { stepCount as stepCount${index}, slideTitle as slideTitle${index}, slideFrontmatter as slideFrontmatter${index}, slideLayout as slideLayout${index} } from ${JSON.stringify(moduleId)};`,
|
|
275
|
+
);
|
|
276
|
+
demoEntries.push(
|
|
277
|
+
`${JSON.stringify(demo.layoutName)}: { mdx: ${JSON.stringify(mdx)}, Component: ${componentName}, stepCount: stepCount${index}, title: slideTitle${index}, frontmatter: slideFrontmatter${index}, layoutName: slideLayout${index} }`,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return [
|
|
282
|
+
...importLines,
|
|
283
|
+
`export { layoutMap };`,
|
|
284
|
+
`export const layoutNames = Object.keys(layoutMap);`,
|
|
285
|
+
`export const layoutDemos = { ${demoEntries.join(", ")} };`,
|
|
286
|
+
`export const layoutPropDocs = { ${propDocEntries.join(", ")} };`,
|
|
287
|
+
`export const layoutDemoWarnings = ${JSON.stringify(crawl.warnings)};`,
|
|
288
|
+
`export default layoutMap;`,
|
|
289
|
+
].join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── virtual:honeydeck/components ────────────────────────────────────────
|
|
293
|
+
if (id === RESOLVED_COMPONENTS_ID) {
|
|
294
|
+
const crawl = crawlComponentDocs({ packageRoot });
|
|
295
|
+
|
|
296
|
+
watchedComponentFiles.clear();
|
|
297
|
+
for (const file of crawl.watchedFiles) {
|
|
298
|
+
watchedComponentFiles.add(file);
|
|
299
|
+
this.addWatchFile(file);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const componentBarrelPath = resolve(
|
|
303
|
+
packageRoot,
|
|
304
|
+
"src/runtime/components/index.ts",
|
|
305
|
+
);
|
|
306
|
+
const importLines: string[] = [
|
|
307
|
+
`import * as componentMap from ${JSON.stringify(toFsImportSpecifier(componentBarrelPath))};`,
|
|
308
|
+
];
|
|
309
|
+
const names = crawl.docs.map((doc) => doc.componentName);
|
|
310
|
+
const docEntries: string[] = [];
|
|
311
|
+
|
|
312
|
+
crawl.docs.forEach((doc, index) => {
|
|
313
|
+
const localName = `ComponentDoc${index}`;
|
|
314
|
+
const moduleId = `${VIRTUAL_COMPONENT_DOC_PREFIX}${encodeURIComponent(doc.componentName)}.mdx`;
|
|
315
|
+
importLines.push(
|
|
316
|
+
`import ${localName} from ${JSON.stringify(moduleId)};`,
|
|
317
|
+
);
|
|
318
|
+
docEntries.push(
|
|
319
|
+
`${JSON.stringify(doc.componentName)}: { Component: ${localName}, markdown: ${JSON.stringify(doc.markdown)}, props: ${JSON.stringify(doc.props)} }`,
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return [
|
|
324
|
+
...importLines,
|
|
325
|
+
`export { componentMap };`,
|
|
326
|
+
`export const componentNames = ${JSON.stringify(names)};`,
|
|
327
|
+
`export const componentDocs = { ${docEntries.join(", ")} };`,
|
|
328
|
+
`export const componentDocWarnings = ${JSON.stringify(crawl.warnings)};`,
|
|
329
|
+
`export default componentMap;`,
|
|
330
|
+
].join("\n");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── virtual:honeydeck/slides ─────────────────────────────────────────────
|
|
334
|
+
if (id === RESOLVED_SLIDES_ID) {
|
|
335
|
+
const { slides } = getResult();
|
|
336
|
+
if (slides.length === 0)
|
|
337
|
+
return "export const slideCount = 0;\nexport {};";
|
|
338
|
+
|
|
339
|
+
const lines: string[] = slides.map(
|
|
340
|
+
(s) =>
|
|
341
|
+
`export { default as Slide${s.index}, stepCount as stepCount${s.index}, slideTitle as slideTitle${s.index}, slideFrontmatter as slideFrontmatter${s.index}, slideLayout as slideLayout${s.index} } from '${VIRTUAL_SLIDE_PREFIX}${s.index}.mdx';`,
|
|
342
|
+
);
|
|
343
|
+
lines.push(`export const slideCount = ${slides.length};`);
|
|
344
|
+
return lines.join("\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── virtual:honeydeck/slide/N.mdx ────────────────────────────────────────
|
|
348
|
+
if (id.startsWith(RESOLVED_SLIDE_PREFIX)) {
|
|
349
|
+
// e.g. '\0virtual:honeydeck/slide/2.mdx' → index 2
|
|
350
|
+
const suffix = id.slice(RESOLVED_SLIDE_PREFIX.length); // '2.mdx'
|
|
351
|
+
const index = parseInt(suffix.replace(".mdx", ""), 10);
|
|
352
|
+
const { slides } = getResult();
|
|
353
|
+
const slide = slides[index];
|
|
354
|
+
|
|
355
|
+
if (!slide) {
|
|
356
|
+
this.error(
|
|
357
|
+
`Honeydeck: slide ${index} not found — deck has ${slides.length} slide(s). ` +
|
|
358
|
+
`Did you delete a slide separator in ${entryFileName}?`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Compile MDX → JS here so @mdx-js/rollup's transform is bypassed.
|
|
363
|
+
// createFilter() from @rollup/pluginutils excludes \0-prefixed virtual
|
|
364
|
+
// IDs by default, so the rollup plugin would never transform these.
|
|
365
|
+
const vfile = await compile(slide.rawMdx, {
|
|
366
|
+
remarkPlugins: [
|
|
367
|
+
remarkFrontmatter,
|
|
368
|
+
remarkGfm,
|
|
369
|
+
remarkH1Extract,
|
|
370
|
+
remarkStepNumbering,
|
|
371
|
+
remarkShikiCodeBlocks,
|
|
372
|
+
],
|
|
373
|
+
jsxImportSource: "react",
|
|
374
|
+
outputFormat: "program",
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Append slide metadata as named exports so the Deck runtime can
|
|
378
|
+
// read title, frontmatter, layout name, and step count without an
|
|
379
|
+
// extra dynamic import.
|
|
380
|
+
const stepCount: number =
|
|
381
|
+
(vfile.data.stepCount as number | undefined) ?? 0;
|
|
382
|
+
const title: string = (vfile.data.title as string | undefined) ?? "";
|
|
383
|
+
const frontmatter: Record<string, unknown> =
|
|
384
|
+
(vfile.data.frontmatter as Record<string, unknown> | undefined) ?? {};
|
|
385
|
+
const layout: string = (frontmatter.layout as string | undefined) ?? "";
|
|
386
|
+
|
|
387
|
+
const js =
|
|
388
|
+
String(vfile) +
|
|
389
|
+
`\nexport const stepCount = ${stepCount};` +
|
|
390
|
+
`\nexport const slideTitle = ${JSON.stringify(title)};` +
|
|
391
|
+
`\nexport const slideFrontmatter = ${JSON.stringify(frontmatter)};` +
|
|
392
|
+
`\nexport const slideLayout = ${JSON.stringify(layout)};\n`;
|
|
393
|
+
|
|
394
|
+
return js;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── virtual:honeydeck/layout-demo/N.mdx ─────────────────────────────────
|
|
398
|
+
if (id.startsWith(RESOLVED_LAYOUT_DEMO_PREFIX)) {
|
|
399
|
+
const suffix = id.slice(RESOLVED_LAYOUT_DEMO_PREFIX.length);
|
|
400
|
+
const index = parseInt(suffix.replace(".mdx", ""), 10);
|
|
401
|
+
const demo = layoutDemoSources[index];
|
|
402
|
+
|
|
403
|
+
if (!demo) {
|
|
404
|
+
this.error(`Honeydeck: layout demo ${index} not found.`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const vfile = await compile(demo.source, {
|
|
408
|
+
remarkPlugins: [
|
|
409
|
+
remarkFrontmatter,
|
|
410
|
+
remarkGfm,
|
|
411
|
+
remarkH1Extract,
|
|
412
|
+
remarkStepNumbering,
|
|
413
|
+
remarkShikiCodeBlocks,
|
|
414
|
+
],
|
|
415
|
+
jsxImportSource: "react",
|
|
416
|
+
outputFormat: "program",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const stepCount: number =
|
|
420
|
+
(vfile.data.stepCount as number | undefined) ?? 0;
|
|
421
|
+
const title: string = (vfile.data.title as string | undefined) ?? "";
|
|
422
|
+
const frontmatter: Record<string, unknown> =
|
|
423
|
+
(vfile.data.frontmatter as Record<string, unknown> | undefined) ?? {};
|
|
424
|
+
const layout: string = (frontmatter.layout as string | undefined) ?? "";
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
String(vfile) +
|
|
428
|
+
`\nexport const stepCount = ${stepCount};` +
|
|
429
|
+
`\nexport const slideTitle = ${JSON.stringify(title)};` +
|
|
430
|
+
`\nexport const slideFrontmatter = ${JSON.stringify(frontmatter)};` +
|
|
431
|
+
`\nexport const slideLayout = ${JSON.stringify(layout)};\n`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── virtual:honeydeck/component-doc/ComponentName.mdx ───────────────────
|
|
436
|
+
if (id.startsWith(RESOLVED_COMPONENT_DOC_PREFIX)) {
|
|
437
|
+
const suffix = id.slice(RESOLVED_COMPONENT_DOC_PREFIX.length);
|
|
438
|
+
const componentName = decodeURIComponent(suffix.replace(".mdx", ""));
|
|
439
|
+
const doc = crawlComponentDocs({ packageRoot }).docs.find(
|
|
440
|
+
(doc) => doc.componentName === componentName,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (!doc) {
|
|
444
|
+
this.error(
|
|
445
|
+
`Honeydeck: component documentation page for "${componentName}" not found.`,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const vfile = await compile(doc.markdown, {
|
|
450
|
+
remarkPlugins: [remarkFrontmatter, remarkGfm, remarkShikiCodeBlocks],
|
|
451
|
+
jsxImportSource: "react",
|
|
452
|
+
outputFormat: "program",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return String(vfile);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return null;
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// ── handleHotUpdate ─────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Called by Vite whenever a watched file changes.
|
|
465
|
+
*
|
|
466
|
+
* Strategy:
|
|
467
|
+
* 1. Ignore changes to files we don't own.
|
|
468
|
+
* 2. Re-split the entry file.
|
|
469
|
+
* 3. Diff previous vs next segments; invalidate only changed virtual modules.
|
|
470
|
+
* 4. Return the affected `ModuleNode` list so Vite can propagate HMR.
|
|
471
|
+
* If no modules were in the graph yet (first edit before any page
|
|
472
|
+
* load), return `undefined` and let Vite do a full reload.
|
|
473
|
+
*/
|
|
474
|
+
handleHotUpdate(ctx: HmrContext): ModuleNode[] | undefined {
|
|
475
|
+
if (
|
|
476
|
+
ctx.file !== entryPath &&
|
|
477
|
+
!watchedMdxFiles.has(ctx.file) &&
|
|
478
|
+
!watchedLayoutFiles.has(ctx.file) &&
|
|
479
|
+
!watchedComponentFiles.has(ctx.file)
|
|
480
|
+
)
|
|
481
|
+
return; // not our file
|
|
482
|
+
|
|
483
|
+
if (watchedLayoutFiles.has(ctx.file)) {
|
|
484
|
+
const affected: ModuleNode[] = [];
|
|
485
|
+
const invalidate = (resolvedId: string): void => {
|
|
486
|
+
const mod = ctx.server.moduleGraph.getModuleById(resolvedId);
|
|
487
|
+
if (mod) {
|
|
488
|
+
ctx.server.moduleGraph.invalidateModule(mod);
|
|
489
|
+
affected.push(mod);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
for (let i = 0; i < layoutDemoSources.length; i++) {
|
|
494
|
+
invalidate(`${RESOLVED_LAYOUT_DEMO_PREFIX}${i}.mdx`);
|
|
495
|
+
}
|
|
496
|
+
const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_LAYOUTS_ID);
|
|
497
|
+
if (mod) {
|
|
498
|
+
ctx.server.moduleGraph.invalidateModule(mod);
|
|
499
|
+
affected.push(mod);
|
|
500
|
+
}
|
|
501
|
+
return affected.length > 0 ? affected : undefined;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (watchedComponentFiles.has(ctx.file)) {
|
|
505
|
+
const affected: ModuleNode[] = [...ctx.modules];
|
|
506
|
+
const invalidate = (resolvedId: string): void => {
|
|
507
|
+
const mod = ctx.server.moduleGraph.getModuleById(resolvedId);
|
|
508
|
+
if (mod) {
|
|
509
|
+
ctx.server.moduleGraph.invalidateModule(mod);
|
|
510
|
+
affected.push(mod);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
invalidate(RESOLVED_COMPONENTS_ID);
|
|
515
|
+
crawlComponentDocs({ packageRoot }).docs.forEach((doc) => {
|
|
516
|
+
invalidate(
|
|
517
|
+
`${RESOLVED_COMPONENT_DOC_PREFIX}${encodeURIComponent(doc.componentName)}.mdx`,
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (affected.length > 0) {
|
|
522
|
+
return affected;
|
|
523
|
+
}
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const oldResult = getResult();
|
|
528
|
+
const newResult = loadAndSplit();
|
|
529
|
+
splitResult = newResult; // commit new cache
|
|
530
|
+
|
|
531
|
+
const affected: ModuleNode[] = [];
|
|
532
|
+
|
|
533
|
+
/** Invalidate a virtual module by its resolved ID, if it's in the graph. */
|
|
534
|
+
const invalidate = (resolvedId: string): void => {
|
|
535
|
+
const mod = ctx.server.moduleGraph.getModuleById(resolvedId);
|
|
536
|
+
if (mod) {
|
|
537
|
+
ctx.server.moduleGraph.invalidateModule(mod);
|
|
538
|
+
affected.push(mod);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Config changed?
|
|
543
|
+
if (
|
|
544
|
+
JSON.stringify(oldResult.deckFrontmatter) !==
|
|
545
|
+
JSON.stringify(newResult.deckFrontmatter)
|
|
546
|
+
) {
|
|
547
|
+
invalidate(RESOLVED_CONFIG_ID);
|
|
548
|
+
invalidate(RESOLVED_LAYOUTS_ID);
|
|
549
|
+
for (let i = 0; i < layoutDemoSources.length; i++) {
|
|
550
|
+
invalidate(`${RESOLVED_LAYOUT_DEMO_PREFIX}${i}.mdx`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Per-slide diff — invalidate only slides that changed or were added/removed.
|
|
555
|
+
const maxLen = Math.max(oldResult.slides.length, newResult.slides.length);
|
|
556
|
+
for (let i = 0; i < maxLen; i++) {
|
|
557
|
+
const oldSlide = oldResult.slides[i];
|
|
558
|
+
const newSlide = newResult.slides[i];
|
|
559
|
+
if (!oldSlide || !newSlide || oldSlide.rawMdx !== newSlide.rawMdx) {
|
|
560
|
+
invalidate(`${RESOLVED_SLIDE_PREFIX}${i}.mdx`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Slide count changed? → barrel module is stale.
|
|
565
|
+
if (oldResult.slides.length !== newResult.slides.length) {
|
|
566
|
+
invalidate(RESOLVED_SLIDES_ID);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Return affected modules for HMR propagation.
|
|
570
|
+
// An empty array would tell Vite "handled, nothing to update" (suppress
|
|
571
|
+
// reload), so we return `undefined` in that case to let it decide.
|
|
572
|
+
return affected.length > 0 ? affected : undefined;
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Re-export IDs as constants so other parts of the plugin suite can import them
|
|
578
|
+
// without duplicating the magic strings.
|
|
579
|
+
export const VIRTUAL_IDS = {
|
|
580
|
+
SLIDE_PREFIX: VIRTUAL_SLIDE_PREFIX,
|
|
581
|
+
SLIDES: VIRTUAL_SLIDES_ID,
|
|
582
|
+
CONFIG: VIRTUAL_CONFIG_ID,
|
|
583
|
+
LAYOUTS: VIRTUAL_LAYOUTS_ID,
|
|
584
|
+
LAYOUT_DEMO_PREFIX: VIRTUAL_LAYOUT_DEMO_PREFIX,
|
|
585
|
+
COMPONENTS: VIRTUAL_COMPONENTS_ID,
|
|
586
|
+
COMPONENT_DOC_PREFIX: VIRTUAL_COMPONENT_DOC_PREFIX,
|
|
587
|
+
} as const;
|