@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.
Files changed (144) hide show
  1. package/AGENTS.md +25 -0
  2. package/DEVELOPMENT.md +522 -0
  3. package/LICENSE +21 -0
  4. package/Readme.md +49 -0
  5. package/SPEC.md +88 -0
  6. package/docs/components.md +63 -0
  7. package/docs/configuration.md +91 -0
  8. package/docs/getting-started.md +116 -0
  9. package/docs/kit-authoring.md +207 -0
  10. package/docs/kits.md +387 -0
  11. package/docs/local-development.md +95 -0
  12. package/docs/mermaid.md +198 -0
  13. package/docs/mobile.md +108 -0
  14. package/docs/navigation.md +93 -0
  15. package/docs/next-steps.md +377 -0
  16. package/docs/pdf-export.md +91 -0
  17. package/docs/presenter-mode.md +104 -0
  18. package/docs/slides.md +130 -0
  19. package/docs/slidev-migration.md +42 -0
  20. package/docs/steps-and-reveals.md +171 -0
  21. package/package.json +134 -0
  22. package/skills/SPEC.md +21 -0
  23. package/skills/honeydeck/SKILL.md +65 -0
  24. package/skills/presentation-writing/SKILL.md +75 -0
  25. package/skills/slidev-migration/SKILL.md +153 -0
  26. package/src/SPEC.md +89 -0
  27. package/src/assets.d.ts +30 -0
  28. package/src/cli/SPEC.md +230 -0
  29. package/src/cli/args.ts +3 -0
  30. package/src/cli/banner.ts +9 -0
  31. package/src/cli/bin.js +5 -0
  32. package/src/cli/build.ts +229 -0
  33. package/src/cli/deck-path.ts +32 -0
  34. package/src/cli/dev.ts +263 -0
  35. package/src/cli/index.ts +126 -0
  36. package/src/cli/init.ts +369 -0
  37. package/src/cli/pdf.ts +923 -0
  38. package/src/cli/skill.ts +75 -0
  39. package/src/cli/templates/SPEC.md +70 -0
  40. package/src/cli/templates/deck-mdx.ts +15 -0
  41. package/src/cli/templates/package-json.ts +36 -0
  42. package/src/cli/templates/sparkle-button.ts +15 -0
  43. package/src/cli/templates/starter/components/SparkleButton.tsx +84 -0
  44. package/src/cli/templates/starter/deck.mdx +153 -0
  45. package/src/cli/templates/starter/styles.css +14 -0
  46. package/src/cli/templates/styles-css.ts +14 -0
  47. package/src/defaults.ts +1 -0
  48. package/src/layouts/ColorModeImage.tsx +55 -0
  49. package/src/layouts/SPEC.md +393 -0
  50. package/src/layouts/SlideFrame.tsx +48 -0
  51. package/src/layouts/bee/Blank.tsx +12 -0
  52. package/src/layouts/bee/Cover.tsx +70 -0
  53. package/src/layouts/bee/Default.tsx +42 -0
  54. package/src/layouts/bee/Image/Image.tsx +151 -0
  55. package/src/layouts/bee/Image/placeholder-dark.webp +0 -0
  56. package/src/layouts/bee/Image/placeholder-vertical-dark.webp +0 -0
  57. package/src/layouts/bee/Image/placeholder-vertical.webp +0 -0
  58. package/src/layouts/bee/Image/placeholder.webp +0 -0
  59. package/src/layouts/bee/ImageLeft.tsx +27 -0
  60. package/src/layouts/bee/ImageRight.tsx +27 -0
  61. package/src/layouts/bee/ImageSide.tsx +107 -0
  62. package/src/layouts/bee/Section.tsx +40 -0
  63. package/src/layouts/bee/TwoCol.tsx +108 -0
  64. package/src/layouts/bee/index.ts +40 -0
  65. package/src/layouts/clean/Blank.tsx +12 -0
  66. package/src/layouts/clean/Cover.tsx +58 -0
  67. package/src/layouts/clean/Default.tsx +33 -0
  68. package/src/layouts/clean/Image/Image.tsx +103 -0
  69. package/src/layouts/clean/ImageLeft.tsx +27 -0
  70. package/src/layouts/clean/ImageRight.tsx +27 -0
  71. package/src/layouts/clean/ImageSide.tsx +113 -0
  72. package/src/layouts/clean/Section.tsx +35 -0
  73. package/src/layouts/clean/TwoCol.tsx +63 -0
  74. package/src/layouts/clean/index.ts +40 -0
  75. package/src/layouts/index.ts +60 -0
  76. package/src/layouts/placeholders.ts +9 -0
  77. package/src/layouts/utils.ts +13 -0
  78. package/src/remark/SPEC.md +49 -0
  79. package/src/remark/h1-extract.ts +124 -0
  80. package/src/remark/index.ts +4 -0
  81. package/src/remark/shiki-code-blocks.ts +325 -0
  82. package/src/remark/step-numbering.ts +412 -0
  83. package/src/runtime/Deck.tsx +533 -0
  84. package/src/runtime/SPEC.md +256 -0
  85. package/src/runtime/SlideCanvas.tsx +95 -0
  86. package/src/runtime/TimelineContext.tsx +122 -0
  87. package/src/runtime/app-shell/index.html +31 -0
  88. package/src/runtime/app-shell/main.tsx +42 -0
  89. package/src/runtime/aspectRatio.ts +34 -0
  90. package/src/runtime/colorMode.ts +23 -0
  91. package/src/runtime/components/BrowserFrame.tsx +233 -0
  92. package/src/runtime/components/Button.tsx +57 -0
  93. package/src/runtime/components/CodeBlock.tsx +210 -0
  94. package/src/runtime/components/ColorModeCycleButton.tsx +59 -0
  95. package/src/runtime/components/ErrorBoundary.tsx +125 -0
  96. package/src/runtime/components/Keyboard.tsx +87 -0
  97. package/src/runtime/components/ListStyle.tsx +203 -0
  98. package/src/runtime/components/NavBar.tsx +223 -0
  99. package/src/runtime/components/NavBarButton.tsx +47 -0
  100. package/src/runtime/components/NavBarDivider.tsx +3 -0
  101. package/src/runtime/components/Notes.tsx +171 -0
  102. package/src/runtime/components/Reveal.tsx +82 -0
  103. package/src/runtime/components/RevealGroup.tsx +193 -0
  104. package/src/runtime/components/SPEC.md +263 -0
  105. package/src/runtime/components/SlideNumberBadge.tsx +11 -0
  106. package/src/runtime/components/TimelineSteps.tsx +115 -0
  107. package/src/runtime/components/index.ts +55 -0
  108. package/src/runtime/index.ts +42 -0
  109. package/src/runtime/inputOwnership.ts +68 -0
  110. package/src/runtime/keyboardTarget.ts +7 -0
  111. package/src/runtime/lastSlideRoute.ts +56 -0
  112. package/src/runtime/navigation.ts +211 -0
  113. package/src/runtime/router.ts +157 -0
  114. package/src/runtime/slideData.ts +137 -0
  115. package/src/runtime/sync.ts +267 -0
  116. package/src/runtime/types.ts +182 -0
  117. package/src/runtime/useKeyboardNav.ts +138 -0
  118. package/src/runtime/useSwipeNav.ts +257 -0
  119. package/src/runtime/views/DocsView.tsx +74 -0
  120. package/src/runtime/views/OverviewView.tsx +386 -0
  121. package/src/runtime/views/PresenterNotesPanel.tsx +76 -0
  122. package/src/runtime/views/PresenterView.tsx +340 -0
  123. package/src/runtime/views/SPEC.md +152 -0
  124. package/src/runtime/views/docs/ComponentsTab.tsx +178 -0
  125. package/src/runtime/views/docs/DocsHeader.tsx +101 -0
  126. package/src/runtime/views/docs/Intro.tsx +20 -0
  127. package/src/runtime/views/docs/LayoutsTab.tsx +324 -0
  128. package/src/runtime/views/docs/ThemeTab.tsx +110 -0
  129. package/src/runtime/views/index.ts +7 -0
  130. package/src/runtime/views/overviewGrid.ts +106 -0
  131. package/src/runtime/views/presenterPreview.ts +27 -0
  132. package/src/runtime/virtual-modules.d.ts +98 -0
  133. package/src/theme/SPEC.md +179 -0
  134. package/src/theme/base.css +623 -0
  135. package/src/theme/bee.css +35 -0
  136. package/src/theme/clean.css +38 -0
  137. package/src/vite-plugin/SPEC.md +114 -0
  138. package/src/vite-plugin/component-doc-crawler.ts +350 -0
  139. package/src/vite-plugin/deck-loader.ts +148 -0
  140. package/src/vite-plugin/index.ts +373 -0
  141. package/src/vite-plugin/layout-demo-crawler.ts +802 -0
  142. package/src/vite-plugin/splitter.ts +353 -0
  143. package/src/vite-plugin/token-manifest.ts +163 -0
  144. package/src/vite-plugin/virtual-modules.ts +587 -0
@@ -0,0 +1,802 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import {
4
+ dirname,
5
+ extname,
6
+ isAbsolute,
7
+ relative,
8
+ resolve,
9
+ sep,
10
+ } from "node:path";
11
+ import ts from "typescript";
12
+
13
+ const SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
14
+
15
+ type ImportBinding = {
16
+ moduleSpecifier: string;
17
+ importedName: "default" | string;
18
+ };
19
+
20
+ type StaticDemoMetadata = {
21
+ mdx?: string;
22
+ dynamicMdx?: boolean;
23
+ };
24
+
25
+ export type LayoutPropDoc = {
26
+ name: string;
27
+ type: string;
28
+ required: boolean;
29
+ description: string;
30
+ };
31
+
32
+ export type DiscoveredLayoutDemo = {
33
+ layoutName: string;
34
+ modulePath: string;
35
+ publicModuleSpecifier: string;
36
+ demoMetadata?: StaticDemoMetadata;
37
+ propDocs: LayoutPropDoc[];
38
+ };
39
+
40
+ export type LayoutDemoCrawlResult = {
41
+ mapPath: string | null;
42
+ demos: DiscoveredLayoutDemo[];
43
+ watchedFiles: string[];
44
+ warnings: string[];
45
+ };
46
+
47
+ export type ResolveLayoutMapOptions = {
48
+ entryPath: string;
49
+ packageRoot: string;
50
+ layoutSpecifier: string;
51
+ };
52
+
53
+ export function resolveLayoutMapPath({
54
+ entryPath,
55
+ packageRoot,
56
+ layoutSpecifier,
57
+ }: ResolveLayoutMapOptions): string | null {
58
+ const specifier = layoutSpecifier.trim();
59
+
60
+ if (!specifier) {
61
+ return resolve(packageRoot, "src/layouts/index.ts");
62
+ }
63
+
64
+ if (specifier === "@honeydeck/honeydeck/layouts") {
65
+ return resolve(packageRoot, "src/layouts/index.ts");
66
+ }
67
+
68
+ if (specifier === "@honeydeck/honeydeck/layouts/clean") {
69
+ return resolve(packageRoot, "src/layouts/clean/index.ts");
70
+ }
71
+
72
+ if (specifier === "@honeydeck/honeydeck/layouts/bee") {
73
+ return resolve(packageRoot, "src/layouts/bee/index.ts");
74
+ }
75
+
76
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
77
+ return resolveSourceFile(resolve(dirname(entryPath), specifier));
78
+ }
79
+
80
+ try {
81
+ const requireFromDeck = createRequire(
82
+ resolve(dirname(entryPath), "__honeydeck_resolve__.js"),
83
+ );
84
+ return resolveSourceFile(requireFromDeck.resolve(specifier));
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ export function crawlLayoutDemos(
91
+ options: ResolveLayoutMapOptions,
92
+ ): LayoutDemoCrawlResult {
93
+ const mapPath = resolveLayoutMapPath(options);
94
+ const context: CrawlContext = {
95
+ ...options,
96
+ watchedFiles: new Set<string>(),
97
+ warnings: [],
98
+ visitedMaps: new Set<string>(),
99
+ };
100
+
101
+ if (!mapPath) {
102
+ context.warnings.push(
103
+ `Could not resolve layout map "${options.layoutSpecifier || "@honeydeck/honeydeck/layouts"}" for demo discovery.`,
104
+ );
105
+ return {
106
+ mapPath: null,
107
+ demos: [],
108
+ watchedFiles: [],
109
+ warnings: context.warnings,
110
+ };
111
+ }
112
+
113
+ const demos = crawlLayoutMapFile(mapPath, context);
114
+ return {
115
+ mapPath,
116
+ demos,
117
+ watchedFiles: [...context.watchedFiles],
118
+ warnings: context.warnings,
119
+ };
120
+ }
121
+
122
+ type CrawlContext = ResolveLayoutMapOptions & {
123
+ watchedFiles: Set<string>;
124
+ warnings: string[];
125
+ visitedMaps: Set<string>;
126
+ };
127
+
128
+ function crawlLayoutMapFile(
129
+ mapPath: string,
130
+ context: CrawlContext,
131
+ ): DiscoveredLayoutDemo[] {
132
+ if (context.visitedMaps.has(mapPath)) return [];
133
+ context.visitedMaps.add(mapPath);
134
+ context.watchedFiles.add(mapPath);
135
+
136
+ let sourceFile: ts.SourceFile;
137
+ try {
138
+ sourceFile = parseFile(mapPath);
139
+ } catch (error) {
140
+ context.warnings.push(
141
+ `Could not read layout map "${mapPath}": ${String(error)}`,
142
+ );
143
+ return [];
144
+ }
145
+
146
+ const bindings = collectImportBindings(sourceFile);
147
+ const exportObject = findDefaultExportObject(sourceFile);
148
+
149
+ if (!exportObject) {
150
+ context.warnings.push(
151
+ `Could not statically read default export object from layout map "${mapPath}".`,
152
+ );
153
+ return [];
154
+ }
155
+
156
+ const demos: DiscoveredLayoutDemo[] = [];
157
+
158
+ for (const property of exportObject.properties) {
159
+ if (ts.isSpreadAssignment(property)) {
160
+ demos.push(...crawlSpreadLayoutMap(property, bindings, mapPath, context));
161
+ continue;
162
+ }
163
+
164
+ if (
165
+ !ts.isPropertyAssignment(property) &&
166
+ !ts.isShorthandPropertyAssignment(property)
167
+ )
168
+ continue;
169
+
170
+ const layoutName = getLayoutName(property);
171
+ const localName = getLayoutLocalIdentifier(property);
172
+ if (!layoutName || !localName) continue;
173
+
174
+ const binding = bindings.get(localName);
175
+ if (!binding) {
176
+ context.warnings.push(
177
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
178
+ );
179
+ continue;
180
+ }
181
+
182
+ const modulePath = resolveImportedModule(
183
+ mapPath,
184
+ binding.moduleSpecifier,
185
+ context.packageRoot,
186
+ );
187
+ if (!modulePath) {
188
+ context.warnings.push(
189
+ `Could not resolve layout module "${binding.moduleSpecifier}" for layout "${layoutName}".`,
190
+ );
191
+ continue;
192
+ }
193
+
194
+ context.watchedFiles.add(modulePath);
195
+ const publicModuleSpecifier = toPublicSpecifier({
196
+ entryPath: context.entryPath,
197
+ packageRoot: context.packageRoot,
198
+ mapPath,
199
+ modulePath,
200
+ originalSpecifier: binding.moduleSpecifier,
201
+ });
202
+
203
+ let demoMetadata: StaticDemoMetadata | undefined;
204
+ try {
205
+ demoMetadata = extractDemoMetadata(modulePath);
206
+ } catch (error) {
207
+ context.warnings.push(
208
+ `Could not inspect demo for layout "${layoutName}": ${String(error)}`,
209
+ );
210
+ }
211
+
212
+ const propDocs = extractLayoutPropDocs(modulePath, context.packageRoot);
213
+ demos.push({
214
+ layoutName,
215
+ modulePath,
216
+ publicModuleSpecifier,
217
+ demoMetadata,
218
+ propDocs,
219
+ });
220
+ }
221
+
222
+ return demos;
223
+ }
224
+
225
+ function crawlSpreadLayoutMap(
226
+ property: ts.SpreadAssignment,
227
+ bindings: Map<string, ImportBinding>,
228
+ mapPath: string,
229
+ context: CrawlContext,
230
+ ): DiscoveredLayoutDemo[] {
231
+ const expression = unwrapExpression(property.expression);
232
+ if (!ts.isIdentifier(expression)) {
233
+ context.warnings.push(
234
+ `Spread layout map in "${mapPath}" is dynamic; demo auto-discovery skipped for that spread.`,
235
+ );
236
+ return [];
237
+ }
238
+
239
+ const binding = bindings.get(expression.text);
240
+ if (!binding) {
241
+ context.warnings.push(
242
+ `Spread layout map "${expression.text}" in "${mapPath}" is not a static import; demo auto-discovery skipped for that spread.`,
243
+ );
244
+ return [];
245
+ }
246
+
247
+ if (binding.importedName !== "default") {
248
+ context.warnings.push(
249
+ `Spread layout map "${expression.text}" in "${mapPath}" is not a default import; demo auto-discovery skipped for that spread.`,
250
+ );
251
+ return [];
252
+ }
253
+
254
+ const spreadMapPath = resolveImportedModule(
255
+ mapPath,
256
+ binding.moduleSpecifier,
257
+ context.packageRoot,
258
+ );
259
+ if (!spreadMapPath) {
260
+ context.warnings.push(
261
+ `Could not resolve spread layout map "${binding.moduleSpecifier}" from "${mapPath}".`,
262
+ );
263
+ return [];
264
+ }
265
+
266
+ return crawlLayoutMapFile(spreadMapPath, context);
267
+ }
268
+
269
+ export function toFsImportSpecifier(path: string): string {
270
+ return `/@fs/${normalizePath(path)}`;
271
+ }
272
+
273
+ function parseFile(path: string): ts.SourceFile {
274
+ const source = readFileSync(path, "utf-8");
275
+ const kind =
276
+ path.endsWith(".tsx") || path.endsWith(".jsx")
277
+ ? ts.ScriptKind.TSX
278
+ : ts.ScriptKind.TS;
279
+ return ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, kind);
280
+ }
281
+
282
+ function collectImportBindings(
283
+ sourceFile: ts.SourceFile,
284
+ ): Map<string, ImportBinding> {
285
+ const bindings = new Map<string, ImportBinding>();
286
+
287
+ for (const statement of sourceFile.statements) {
288
+ if (!ts.isImportDeclaration(statement)) continue;
289
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
290
+
291
+ const moduleSpecifier = statement.moduleSpecifier.text;
292
+ const clause = statement.importClause;
293
+ if (!clause) continue;
294
+
295
+ if (clause.name) {
296
+ bindings.set(clause.name.text, {
297
+ moduleSpecifier,
298
+ importedName: "default",
299
+ });
300
+ }
301
+
302
+ const namedBindings = clause.namedBindings;
303
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
304
+ for (const element of namedBindings.elements) {
305
+ bindings.set(element.name.text, {
306
+ moduleSpecifier,
307
+ importedName: element.propertyName?.text ?? element.name.text,
308
+ });
309
+ }
310
+ }
311
+ }
312
+
313
+ return bindings;
314
+ }
315
+
316
+ function findDefaultExportObject(
317
+ sourceFile: ts.SourceFile,
318
+ ): ts.ObjectLiteralExpression | null {
319
+ const variables = new Map<string, ts.Expression>();
320
+
321
+ for (const statement of sourceFile.statements) {
322
+ if (ts.isVariableStatement(statement)) {
323
+ for (const declaration of statement.declarationList.declarations) {
324
+ if (ts.isIdentifier(declaration.name) && declaration.initializer) {
325
+ variables.set(declaration.name.text, declaration.initializer);
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ for (const statement of sourceFile.statements) {
332
+ if (ts.isExportAssignment(statement)) {
333
+ const expression = unwrapExpression(statement.expression);
334
+ if (ts.isObjectLiteralExpression(expression)) return expression;
335
+ if (ts.isIdentifier(expression)) {
336
+ const initializer = variables.get(expression.text);
337
+ const unwrapped = initializer ? unwrapExpression(initializer) : null;
338
+ if (unwrapped && ts.isObjectLiteralExpression(unwrapped))
339
+ return unwrapped;
340
+ }
341
+ }
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ function unwrapExpression(expression: ts.Expression): ts.Expression {
348
+ let current = expression;
349
+ while (
350
+ ts.isAsExpression(current) ||
351
+ ts.isSatisfiesExpression(current) ||
352
+ ts.isParenthesizedExpression(current) ||
353
+ ts.isTypeAssertionExpression(current)
354
+ ) {
355
+ current = current.expression;
356
+ }
357
+ return current;
358
+ }
359
+
360
+ function getLayoutName(
361
+ property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
362
+ ): string | null {
363
+ if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
364
+
365
+ const name = property.name;
366
+ if (
367
+ ts.isIdentifier(name) ||
368
+ ts.isStringLiteral(name) ||
369
+ ts.isNumericLiteral(name)
370
+ )
371
+ return name.text;
372
+ return null;
373
+ }
374
+
375
+ function getLayoutLocalIdentifier(
376
+ property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
377
+ ): string | null {
378
+ if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
379
+
380
+ const initializer = unwrapExpression(property.initializer);
381
+ return ts.isIdentifier(initializer) ? initializer.text : null;
382
+ }
383
+
384
+ function resolveImportedModule(
385
+ fromFile: string,
386
+ specifier: string,
387
+ packageRoot: string,
388
+ ): string | null {
389
+ if (specifier === "@honeydeck/honeydeck/layouts") {
390
+ return resolve(packageRoot, "src/layouts/index.ts");
391
+ }
392
+
393
+ if (specifier === "@honeydeck/honeydeck/layouts/clean") {
394
+ return resolve(packageRoot, "src/layouts/clean/index.ts");
395
+ }
396
+
397
+ if (specifier === "@honeydeck/honeydeck/layouts/bee") {
398
+ return resolve(packageRoot, "src/layouts/bee/index.ts");
399
+ }
400
+
401
+ if (specifier.startsWith("@honeydeck/honeydeck/layouts/clean/")) {
402
+ const layoutName = specifier.slice(
403
+ "@honeydeck/honeydeck/layouts/clean/".length,
404
+ );
405
+ const builtInPath =
406
+ layoutName === "Image"
407
+ ? resolve(packageRoot, "src/layouts/clean/Image/Image.tsx")
408
+ : resolve(packageRoot, `src/layouts/clean/${layoutName}.tsx`);
409
+ return resolveSourceFile(builtInPath);
410
+ }
411
+
412
+ if (specifier.startsWith("@honeydeck/honeydeck/layouts/bee/")) {
413
+ const layoutName = specifier.slice(
414
+ "@honeydeck/honeydeck/layouts/bee/".length,
415
+ );
416
+ const builtInPath =
417
+ layoutName === "Image"
418
+ ? resolve(packageRoot, "src/layouts/bee/Image/Image.tsx")
419
+ : resolve(packageRoot, `src/layouts/bee/${layoutName}.tsx`);
420
+ return resolveSourceFile(builtInPath);
421
+ }
422
+
423
+ if (specifier.startsWith("@honeydeck/honeydeck/layouts/")) {
424
+ const layoutName = specifier.slice("@honeydeck/honeydeck/layouts/".length);
425
+ const builtInPath =
426
+ layoutName === "Image"
427
+ ? resolve(packageRoot, "src/layouts/clean/Image/Image.tsx")
428
+ : resolve(packageRoot, `src/layouts/clean/${layoutName}.tsx`);
429
+ return resolveSourceFile(builtInPath);
430
+ }
431
+
432
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
433
+ return resolveSourceFile(resolve(dirname(fromFile), specifier));
434
+ }
435
+
436
+ if (specifier.startsWith("/@fs/"))
437
+ return resolveSourceFile(specifier.slice("/@fs/".length));
438
+ if (isAbsolute(specifier)) return resolveSourceFile(specifier);
439
+
440
+ try {
441
+ const requireFromMap = createRequire(fromFile);
442
+ return resolveSourceFile(requireFromMap.resolve(specifier));
443
+ } catch {
444
+ return null;
445
+ }
446
+ }
447
+
448
+ function resolveSourceFile(basePath: string): string | null {
449
+ if (existsSync(basePath) && extname(basePath)) return basePath;
450
+
451
+ for (const extension of SOURCE_EXTENSIONS) {
452
+ const candidate = `${basePath}${extension}`;
453
+ if (existsSync(candidate)) return candidate;
454
+ }
455
+
456
+ if (existsSync(basePath) && !extname(basePath)) {
457
+ for (const extension of SOURCE_EXTENSIONS) {
458
+ const candidate = resolve(basePath, `index${extension}`);
459
+ if (existsSync(candidate)) return candidate;
460
+ }
461
+ }
462
+
463
+ return existsSync(basePath) ? basePath : null;
464
+ }
465
+
466
+ function extractDemoMetadata(
467
+ modulePath: string,
468
+ ): StaticDemoMetadata | undefined {
469
+ const sourceFile = parseFile(modulePath);
470
+ const demoInitializer = findExportedDemoInitializer(sourceFile);
471
+ if (!demoInitializer) return undefined;
472
+
473
+ const demoObject = unwrapExpression(demoInitializer);
474
+ if (!ts.isObjectLiteralExpression(demoObject)) return { dynamicMdx: true };
475
+
476
+ const result: StaticDemoMetadata = {};
477
+
478
+ for (const property of demoObject.properties) {
479
+ if (!ts.isPropertyAssignment(property)) continue;
480
+ const name = propertyNameText(property.name);
481
+ if (!name) continue;
482
+
483
+ const initializer = unwrapExpression(property.initializer);
484
+
485
+ if (name === "mdx") {
486
+ const text = staticPrimitive(initializer);
487
+ if (typeof text === "string") {
488
+ result.mdx = text;
489
+ } else {
490
+ result.dynamicMdx = true;
491
+ }
492
+ }
493
+ }
494
+
495
+ return result;
496
+ }
497
+
498
+ function extractLayoutPropDocs(
499
+ modulePath: string,
500
+ packageRoot: string,
501
+ ): LayoutPropDoc[] {
502
+ let sourceFile: ts.SourceFile;
503
+ try {
504
+ sourceFile = parseFile(modulePath);
505
+ } catch {
506
+ return [];
507
+ }
508
+
509
+ const frontmatterType = findFirstLayoutPropsTypeArgument(sourceFile);
510
+ if (!frontmatterType) return [];
511
+
512
+ return extractPropDocsFromTypeNode(
513
+ frontmatterType,
514
+ sourceFile,
515
+ modulePath,
516
+ packageRoot,
517
+ );
518
+ }
519
+
520
+ function findFirstLayoutPropsTypeArgument(
521
+ sourceFile: ts.SourceFile,
522
+ ): ts.TypeNode | null {
523
+ let result: ts.TypeNode | null = null;
524
+
525
+ function visit(node: ts.Node): void {
526
+ if (result) return;
527
+ if (
528
+ ts.isTypeReferenceNode(node) &&
529
+ node.typeArguments?.[0] &&
530
+ typeReferenceName(node.typeName) === "LayoutProps"
531
+ ) {
532
+ result = node.typeArguments[0];
533
+ return;
534
+ }
535
+
536
+ ts.forEachChild(node, visit);
537
+ }
538
+
539
+ visit(sourceFile);
540
+ return result;
541
+ }
542
+
543
+ function extractPropDocsFromTypeNode(
544
+ typeNode: ts.TypeNode,
545
+ sourceFile: ts.SourceFile,
546
+ modulePath: string,
547
+ packageRoot: string,
548
+ visited = new Set<string>(),
549
+ ): LayoutPropDoc[] {
550
+ if (ts.isTypeLiteralNode(typeNode)) {
551
+ return typeNode.members.flatMap((member) => {
552
+ if (!ts.isPropertySignature(member)) return [];
553
+ const name = propertyNameText(member.name);
554
+ if (!name) return [];
555
+
556
+ return [
557
+ {
558
+ name,
559
+ type: normalizeTypeText(
560
+ member.type ? member.type.getText(sourceFile) : "unknown",
561
+ ),
562
+ required: !member.questionToken,
563
+ description: jsDocDescription(member),
564
+ },
565
+ ];
566
+ });
567
+ }
568
+
569
+ if (ts.isIntersectionTypeNode(typeNode)) {
570
+ return dedupePropDocs(
571
+ typeNode.types.flatMap((part) =>
572
+ extractPropDocsFromTypeNode(
573
+ part,
574
+ sourceFile,
575
+ modulePath,
576
+ packageRoot,
577
+ visited,
578
+ ),
579
+ ),
580
+ );
581
+ }
582
+
583
+ if (ts.isTypeReferenceNode(typeNode)) {
584
+ const name = typeReferenceName(typeNode.typeName);
585
+ if (!name) return [];
586
+
587
+ const localKey = `${modulePath}#${name}`;
588
+ if (visited.has(localKey)) return [];
589
+ visited.add(localKey);
590
+
591
+ const localAlias = findTypeAlias(sourceFile, name);
592
+ if (localAlias) {
593
+ return extractPropDocsFromTypeNode(
594
+ localAlias.type,
595
+ sourceFile,
596
+ modulePath,
597
+ packageRoot,
598
+ visited,
599
+ );
600
+ }
601
+
602
+ const binding = collectImportBindings(sourceFile).get(name);
603
+ if (!binding) return [];
604
+
605
+ const importedModulePath = resolveImportedModule(
606
+ modulePath,
607
+ binding.moduleSpecifier,
608
+ packageRoot,
609
+ );
610
+ if (!importedModulePath) return [];
611
+
612
+ let importedSourceFile: ts.SourceFile;
613
+ try {
614
+ importedSourceFile = parseFile(importedModulePath);
615
+ } catch {
616
+ return [];
617
+ }
618
+
619
+ const importedName =
620
+ binding.importedName === "default" ? name : binding.importedName;
621
+ const importedAlias = findTypeAlias(importedSourceFile, importedName);
622
+ if (!importedAlias) return [];
623
+
624
+ return extractPropDocsFromTypeNode(
625
+ importedAlias.type,
626
+ importedSourceFile,
627
+ importedModulePath,
628
+ packageRoot,
629
+ visited,
630
+ );
631
+ }
632
+
633
+ return [];
634
+ }
635
+
636
+ function typeReferenceName(typeName: ts.EntityName): string | null {
637
+ if (ts.isIdentifier(typeName)) return typeName.text;
638
+ return typeName.right.text;
639
+ }
640
+
641
+ function findTypeAlias(
642
+ sourceFile: ts.SourceFile,
643
+ name: string,
644
+ ): ts.TypeAliasDeclaration | null {
645
+ return (
646
+ sourceFile.statements.find(
647
+ (statement): statement is ts.TypeAliasDeclaration =>
648
+ ts.isTypeAliasDeclaration(statement) && statement.name.text === name,
649
+ ) ?? null
650
+ );
651
+ }
652
+
653
+ function normalizeTypeText(type: string): string {
654
+ return type.replace(/\s+/g, " ").trim();
655
+ }
656
+
657
+ function jsDocDescription(node: ts.Node): string {
658
+ const jsDoc = (node as { jsDoc?: Array<{ comment?: unknown }> }).jsDoc?.[0];
659
+ const comment = jsDoc?.comment;
660
+ if (typeof comment === "string") return comment.trim();
661
+ if (Array.isArray(comment)) {
662
+ return comment
663
+ .map((part) => ("text" in part ? String(part.text) : ""))
664
+ .join("")
665
+ .trim();
666
+ }
667
+ return "";
668
+ }
669
+
670
+ function dedupePropDocs(propDocs: LayoutPropDoc[]): LayoutPropDoc[] {
671
+ const seen = new Set<string>();
672
+ const result: LayoutPropDoc[] = [];
673
+
674
+ for (const prop of propDocs) {
675
+ if (seen.has(prop.name)) continue;
676
+ seen.add(prop.name);
677
+ result.push(prop);
678
+ }
679
+
680
+ return result;
681
+ }
682
+
683
+ function findExportedDemoInitializer(
684
+ sourceFile: ts.SourceFile,
685
+ ): ts.Expression | null {
686
+ for (const statement of sourceFile.statements) {
687
+ if (!ts.isVariableStatement(statement)) continue;
688
+ const exported = statement.modifiers?.some(
689
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
690
+ );
691
+ if (!exported) continue;
692
+
693
+ for (const declaration of statement.declarationList.declarations) {
694
+ if (
695
+ !ts.isIdentifier(declaration.name) ||
696
+ declaration.name.text !== "demo" ||
697
+ !declaration.initializer
698
+ )
699
+ continue;
700
+ return declaration.initializer;
701
+ }
702
+ }
703
+
704
+ return null;
705
+ }
706
+
707
+ function propertyNameText(name: ts.PropertyName): string | null {
708
+ if (
709
+ ts.isIdentifier(name) ||
710
+ ts.isStringLiteral(name) ||
711
+ ts.isNumericLiteral(name)
712
+ )
713
+ return name.text;
714
+ return null;
715
+ }
716
+
717
+ function staticPrimitive(
718
+ expression: ts.Expression,
719
+ ): string | number | boolean | undefined {
720
+ if (
721
+ ts.isStringLiteral(expression) ||
722
+ ts.isNoSubstitutionTemplateLiteral(expression)
723
+ )
724
+ return expression.text;
725
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
726
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
727
+ if (ts.isNumericLiteral(expression)) return Number(expression.text);
728
+ return undefined;
729
+ }
730
+
731
+ function toPublicSpecifier({
732
+ entryPath,
733
+ packageRoot,
734
+ mapPath,
735
+ modulePath,
736
+ originalSpecifier,
737
+ }: {
738
+ entryPath: string;
739
+ packageRoot: string;
740
+ mapPath: string;
741
+ modulePath: string;
742
+ originalSpecifier: string;
743
+ }): string {
744
+ const builtInLayoutsDir = resolve(packageRoot, "src/layouts");
745
+ const relToBuiltIn = relative(builtInLayoutsDir, modulePath);
746
+ if (!relToBuiltIn.startsWith("..") && !isAbsolute(relToBuiltIn)) {
747
+ const withoutExt = stripExtension(normalizePath(relToBuiltIn));
748
+ const mapRel = stripExtension(
749
+ normalizePath(relative(builtInLayoutsDir, mapPath)),
750
+ );
751
+ const layoutSubpath = withoutExt === "Image/Image" ? "Image" : withoutExt;
752
+
753
+ if (withoutExt === "index") return "@honeydeck/honeydeck/layouts";
754
+ if (withoutExt === "clean/index")
755
+ return "@honeydeck/honeydeck/layouts/clean";
756
+ if (withoutExt === "bee/index") return "@honeydeck/honeydeck/layouts/bee";
757
+
758
+ if (withoutExt.startsWith("clean/")) {
759
+ const cleanSubpath =
760
+ withoutExt === "clean/Image/Image"
761
+ ? "Image"
762
+ : withoutExt.slice("clean/".length);
763
+ if (
764
+ originalSpecifier.startsWith("@honeydeck/honeydeck/layouts/clean") ||
765
+ mapRel.startsWith("clean/")
766
+ ) {
767
+ return `@honeydeck/honeydeck/layouts/clean/${cleanSubpath}`;
768
+ }
769
+ return `@honeydeck/honeydeck/layouts/${cleanSubpath}`;
770
+ }
771
+
772
+ if (withoutExt.startsWith("bee/")) {
773
+ const beeSubpath =
774
+ withoutExt === "bee/Image/Image"
775
+ ? "Image"
776
+ : withoutExt.slice("bee/".length);
777
+ return `@honeydeck/honeydeck/layouts/bee/${beeSubpath}`;
778
+ }
779
+
780
+ return `@honeydeck/honeydeck/layouts/bee/${layoutSubpath}`;
781
+ }
782
+
783
+ if (
784
+ originalSpecifier.startsWith("./") ||
785
+ originalSpecifier.startsWith("../") ||
786
+ isAbsolute(modulePath)
787
+ ) {
788
+ const relToDeck = normalizePath(relative(dirname(entryPath), modulePath));
789
+ const withDot = relToDeck.startsWith(".") ? relToDeck : `./${relToDeck}`;
790
+ return stripExtension(withDot);
791
+ }
792
+
793
+ return originalSpecifier;
794
+ }
795
+
796
+ function stripExtension(path: string): string {
797
+ return path.replace(/\.(tsx|ts|jsx|js)$/, "");
798
+ }
799
+
800
+ function normalizePath(path: string): string {
801
+ return path.split(sep).join("/");
802
+ }