@honeydeck/honeydeck 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honeydeck/honeydeck",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MDX and React-based presentation framework for AI-friendly slide decks.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -180,7 +180,7 @@ Building the future of presentations.`,
180
180
  }
181
181
  ```
182
182
 
183
- `mdx` is required on `LayoutDemo` and is the single source for both the live visual preview and the copyable snippet shown in the layouts docs tab. Honeydeck compiles this MDX with the same slide MDX compiler family, extracts frontmatter/title/steps from it, and renders the resulting slide through the active layout map. Honeydeck statically crawls analyzable active layout maps at build time and discovers colocated `demo` exports from layout modules. Dynamic maps, computed entries, non-static imports, and demos whose `mdx` value is not a static string may be skipped with warnings. If no static MDX demo is discovered, the layout still appears in the reference pages with a "No demo MDX provided" hint.
183
+ `mdx` is required on `LayoutDemo` and is the single source for both the live visual preview and the copyable snippet shown in the layouts docs tab. Honeydeck compiles this MDX with the same slide MDX compiler family, extracts frontmatter/title/steps from it, and renders the resulting slide through the active layout map. Honeydeck statically crawls analyzable active layout maps at build time and discovers colocated `demo` exports from layout modules. Analyzable map entries include direct static imports, named imports from layout barrels, spread static default imports, and static member references into an imported layout map such as `Default: defaultLayouts.Default`. Dynamic maps, computed entries, non-static imports, and demos whose `mdx` value is not a static string may be skipped with warnings. If no static MDX demo is discovered, the layout still appears in the reference pages with a "No demo MDX provided" hint.
184
184
 
185
185
  ### TwoCol Slot Components
186
186
 
@@ -168,37 +168,18 @@ function crawlLayoutMapFile(
168
168
  continue;
169
169
 
170
170
  const layoutName = getLayoutName(property);
171
- const localName = getLayoutLocalIdentifier(property);
172
- if (!layoutName || !localName) continue;
171
+ if (!layoutName) continue;
173
172
 
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,
173
+ const reference = resolveLayoutModuleReference({
174
+ property,
175
+ layoutName,
176
+ bindings,
198
177
  mapPath,
199
- modulePath,
200
- originalSpecifier: binding.moduleSpecifier,
178
+ context,
201
179
  });
180
+ if (!reference) continue;
181
+
182
+ const { modulePath, publicModuleSpecifier } = reference;
202
183
 
203
184
  let demoMetadata: StaticDemoMetadata | undefined;
204
185
  try {
@@ -372,13 +353,303 @@ function getLayoutName(
372
353
  return null;
373
354
  }
374
355
 
375
- function getLayoutLocalIdentifier(
376
- property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment,
356
+ type LayoutModuleReference = Pick<
357
+ DiscoveredLayoutDemo,
358
+ "modulePath" | "publicModuleSpecifier"
359
+ >;
360
+
361
+ function resolveLayoutModuleReference({
362
+ property,
363
+ layoutName,
364
+ bindings,
365
+ mapPath,
366
+ context,
367
+ }: {
368
+ property: ts.PropertyAssignment | ts.ShorthandPropertyAssignment;
369
+ layoutName: string;
370
+ bindings: Map<string, ImportBinding>;
371
+ mapPath: string;
372
+ context: CrawlContext;
373
+ }): LayoutModuleReference | null {
374
+ const value = ts.isShorthandPropertyAssignment(property)
375
+ ? property.name
376
+ : unwrapExpression(property.initializer);
377
+
378
+ if (ts.isIdentifier(value)) {
379
+ return resolveImportedLayoutReference(
380
+ value.text,
381
+ layoutName,
382
+ bindings,
383
+ mapPath,
384
+ context,
385
+ );
386
+ }
387
+
388
+ if (ts.isPropertyAccessExpression(value)) {
389
+ return resolveLayoutMapMemberReference(
390
+ value,
391
+ layoutName,
392
+ bindings,
393
+ mapPath,
394
+ context,
395
+ );
396
+ }
397
+
398
+ context.warnings.push(
399
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
400
+ );
401
+ return null;
402
+ }
403
+
404
+ function resolveImportedLayoutReference(
405
+ localName: string,
406
+ layoutName: string,
407
+ bindings: Map<string, ImportBinding>,
408
+ mapPath: string,
409
+ context: CrawlContext,
410
+ ): LayoutModuleReference | null {
411
+ const binding = bindings.get(localName);
412
+ if (!binding) {
413
+ context.warnings.push(
414
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
415
+ );
416
+ return null;
417
+ }
418
+
419
+ const importedModulePath = resolveImportedModule(
420
+ mapPath,
421
+ binding.moduleSpecifier,
422
+ context.packageRoot,
423
+ );
424
+ if (!importedModulePath) {
425
+ context.warnings.push(
426
+ `Could not resolve layout module "${binding.moduleSpecifier}" for layout "${layoutName}".`,
427
+ );
428
+ return null;
429
+ }
430
+
431
+ context.watchedFiles.add(importedModulePath);
432
+ const modulePath =
433
+ binding.importedName === "default"
434
+ ? importedModulePath
435
+ : (resolveNamedExportModulePath(
436
+ importedModulePath,
437
+ binding.importedName,
438
+ context.packageRoot,
439
+ context.watchedFiles,
440
+ ) ?? importedModulePath);
441
+ context.watchedFiles.add(modulePath);
442
+
443
+ return {
444
+ modulePath,
445
+ publicModuleSpecifier: toPublicSpecifier({
446
+ entryPath: context.entryPath,
447
+ packageRoot: context.packageRoot,
448
+ mapPath,
449
+ modulePath,
450
+ originalSpecifier: binding.moduleSpecifier,
451
+ }),
452
+ };
453
+ }
454
+
455
+ function resolveLayoutMapMemberReference(
456
+ memberExpression: ts.PropertyAccessExpression,
457
+ layoutName: string,
458
+ bindings: Map<string, ImportBinding>,
459
+ mapPath: string,
460
+ context: CrawlContext,
461
+ ): LayoutModuleReference | null {
462
+ const mapIdentifier = unwrapExpression(memberExpression.expression);
463
+ if (
464
+ !ts.isIdentifier(mapIdentifier) ||
465
+ !ts.isIdentifier(memberExpression.name)
466
+ ) {
467
+ context.warnings.push(
468
+ `Layout "${layoutName}" is not backed by a static import; demo auto-discovery skipped.`,
469
+ );
470
+ return null;
471
+ }
472
+
473
+ const binding = bindings.get(mapIdentifier.text);
474
+ if (!binding) {
475
+ context.warnings.push(
476
+ `Layout "${layoutName}" references layout map "${mapIdentifier.text}" without a static import; demo auto-discovery skipped.`,
477
+ );
478
+ return null;
479
+ }
480
+
481
+ if (binding.importedName !== "default") {
482
+ context.warnings.push(
483
+ `Layout "${layoutName}" references layout map "${mapIdentifier.text}", but only default-imported layout maps can be inspected.`,
484
+ );
485
+ return null;
486
+ }
487
+
488
+ const memberMapPath = resolveImportedModule(
489
+ mapPath,
490
+ binding.moduleSpecifier,
491
+ context.packageRoot,
492
+ );
493
+ if (!memberMapPath) {
494
+ context.warnings.push(
495
+ `Could not resolve layout map "${binding.moduleSpecifier}" for layout "${layoutName}".`,
496
+ );
497
+ return null;
498
+ }
499
+
500
+ const memberName = memberExpression.name.text;
501
+ const lookupContext = {
502
+ ...context,
503
+ visitedMaps: new Set<string>(),
504
+ };
505
+ const reference = crawlLayoutMapFile(memberMapPath, lookupContext).find(
506
+ (demo) => demo.layoutName === memberName,
507
+ );
508
+
509
+ if (!reference) {
510
+ context.warnings.push(
511
+ `Could not statically find layout "${memberName}" in layout map "${binding.moduleSpecifier}" for layout "${layoutName}".`,
512
+ );
513
+ }
514
+
515
+ return reference ?? null;
516
+ }
517
+
518
+ function resolveNamedExportModulePath(
519
+ modulePath: string,
520
+ exportedName: string,
521
+ packageRoot: string,
522
+ watchedFiles?: Set<string>,
523
+ visited = new Set<string>(),
377
524
  ): string | null {
378
- if (ts.isShorthandPropertyAssignment(property)) return property.name.text;
525
+ const visitKey = `${modulePath}#${exportedName}`;
526
+ if (visited.has(visitKey)) return null;
527
+ visited.add(visitKey);
528
+ watchedFiles?.add(modulePath);
529
+
530
+ let sourceFile: ts.SourceFile;
531
+ try {
532
+ sourceFile = parseFile(modulePath);
533
+ } catch {
534
+ return null;
535
+ }
536
+
537
+ for (const statement of sourceFile.statements) {
538
+ if (!ts.isExportDeclaration(statement)) continue;
539
+
540
+ if (!statement.exportClause) {
541
+ if (
542
+ !statement.moduleSpecifier ||
543
+ !ts.isStringLiteral(statement.moduleSpecifier)
544
+ )
545
+ continue;
546
+
547
+ const starModulePath = resolveImportedModule(
548
+ modulePath,
549
+ statement.moduleSpecifier.text,
550
+ packageRoot,
551
+ );
552
+ if (!starModulePath) continue;
553
+ watchedFiles?.add(starModulePath);
554
+ const resolved = resolveNamedExportModulePath(
555
+ starModulePath,
556
+ exportedName,
557
+ packageRoot,
558
+ watchedFiles,
559
+ visited,
560
+ );
561
+ if (resolved) return resolved;
562
+ continue;
563
+ }
564
+
565
+ if (!ts.isNamedExports(statement.exportClause)) continue;
566
+
567
+ for (const element of statement.exportClause.elements) {
568
+ if (element.name.text !== exportedName) continue;
569
+
570
+ if (
571
+ statement.moduleSpecifier &&
572
+ ts.isStringLiteral(statement.moduleSpecifier)
573
+ ) {
574
+ const importedModulePath = resolveImportedModule(
575
+ modulePath,
576
+ statement.moduleSpecifier.text,
577
+ packageRoot,
578
+ );
579
+ if (!importedModulePath) return null;
580
+ watchedFiles?.add(importedModulePath);
581
+
582
+ const importedName = element.propertyName?.text ?? element.name.text;
583
+ if (importedName === "default") return importedModulePath;
584
+
585
+ return (
586
+ resolveNamedExportModulePath(
587
+ importedModulePath,
588
+ importedName,
589
+ packageRoot,
590
+ watchedFiles,
591
+ visited,
592
+ ) ?? importedModulePath
593
+ );
594
+ }
595
+
596
+ const localName = element.propertyName?.text ?? element.name.text;
597
+ const binding = collectImportBindings(sourceFile).get(localName);
598
+ if (!binding) return modulePath;
599
+
600
+ const importedModulePath = resolveImportedModule(
601
+ modulePath,
602
+ binding.moduleSpecifier,
603
+ packageRoot,
604
+ );
605
+ if (!importedModulePath) return null;
606
+ if (binding.importedName === "default") return importedModulePath;
607
+
608
+ watchedFiles?.add(importedModulePath);
609
+ return (
610
+ resolveNamedExportModulePath(
611
+ importedModulePath,
612
+ binding.importedName,
613
+ packageRoot,
614
+ watchedFiles,
615
+ visited,
616
+ ) ?? importedModulePath
617
+ );
618
+ }
619
+ }
379
620
 
380
- const initializer = unwrapExpression(property.initializer);
381
- return ts.isIdentifier(initializer) ? initializer.text : null;
621
+ for (const statement of sourceFile.statements) {
622
+ if (!hasExportModifier(statement)) continue;
623
+ if (
624
+ (ts.isFunctionDeclaration(statement) ||
625
+ ts.isClassDeclaration(statement)) &&
626
+ statement.name?.text === exportedName
627
+ ) {
628
+ return modulePath;
629
+ }
630
+
631
+ if (ts.isVariableStatement(statement)) {
632
+ for (const declaration of statement.declarationList.declarations) {
633
+ if (
634
+ ts.isIdentifier(declaration.name) &&
635
+ declaration.name.text === exportedName
636
+ )
637
+ return modulePath;
638
+ }
639
+ }
640
+ }
641
+
642
+ return null;
643
+ }
644
+
645
+ function hasExportModifier(node: ts.Node): boolean {
646
+ return (
647
+ ts.canHaveModifiers(node) &&
648
+ (ts
649
+ .getModifiers(node)
650
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ??
651
+ false)
652
+ );
382
653
  }
383
654
 
384
655
  function resolveImportedModule(