@honeydeck/honeydeck 0.2.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/Readme.md CHANGED
@@ -30,6 +30,7 @@ Decks are plain MDX files separated into slides with `---`; see the first deck e
30
30
  - [Presenter mode](docs/presenter-mode.md) - notes, presenter window, sync, and mobile behavior
31
31
  - [PDF export](docs/pdf-export.md) - options, color modes, and step handling
32
32
  - [Local development](docs/local-development.md) - running Honeydeck from this repository
33
+ - [Skills](docs/skills.md) - optional agent skills for authoring, writing, and migration help
33
34
  - [Slidev migration](docs/slidev-migration.md) - moving from Slidev with the bundled agent skill
34
35
 
35
36
  ## Common commands
@@ -362,18 +362,11 @@ type LayoutProps<F = Record<string, unknown>> = {
362
362
 
363
363
  ## Agent skills
364
364
 
365
- Honeydeck ships optional agent skills:
366
-
367
- - `honeydeck` for Honeydeck-specific MDX and CLI guidance
368
- - `presentation-writing` for help writing strong slide narratives
369
- - `slidev-migration` for moving decks from Slidev to Honeydeck
370
-
371
- `honeydeck init` can open the same interactive skills installer as `honeydeck skill`.
365
+ Honeydeck ships optional agent skills for Honeydeck authoring, presentation writing, and Slidev migration. `honeydeck init` can open the same interactive skills installer as `honeydeck skill`.
372
366
 
373
367
  ```bash
374
368
  honeydeck skill
375
- npx skills add <honeydeck-repo-url> --copy
376
- npx skills add <honeydeck-repo-url> --copy --skill slidev-migration
377
369
  ```
378
370
 
371
+ See [Skills](skills.md) for installation options and bundled skill details.
379
372
  Coming from Slidev? See the [Slidev migration guide](slidev-migration.md).
@@ -109,6 +109,7 @@ honeydeck skill # install optional Honeydeck agent skills
109
109
  - [Presenter mode](presenter-mode.md) - notes, presenter window, sync, and mobile behavior
110
110
  - [PDF export](pdf-export.md) - options, color modes, and step handling
111
111
  - [Local development](local-development.md) - running Honeydeck from this repository
112
+ - [Skills](skills.md) - optional agent skills for authoring, writing, and migration help
112
113
  - [Slidev migration](slidev-migration.md) - moving from Slidev with the bundled agent skill
113
114
 
114
115
  ## Learn inside a running deck
package/docs/skills.md ADDED
@@ -0,0 +1,65 @@
1
+ # Skills
2
+
3
+ Honeydeck ships optional agent skills that help AI coding agents work with presentations more reliably. They are plain skill files bundled with the `@honeydeck/honeydeck` package, so you can install them into a deck project or into your global agent setup.
4
+
5
+ ## Install skills
6
+
7
+ New projects can open the skills installer during init:
8
+
9
+ ```bash
10
+ npx @honeydeck/honeydeck init --name my-talk --install-skill
11
+ ```
12
+
13
+ Existing projects can run:
14
+
15
+ ```bash
16
+ honeydeck skill
17
+ ```
18
+
19
+ Both commands open the same `skills` CLI flow, where you choose which Honeydeck skills to install, whether to install them for the project or globally, and which agents should receive them.
20
+
21
+ You can also install from the Honeydeck repository:
22
+
23
+ ```bash
24
+ npx skills add <honeydeck-repo-url> --copy
25
+ npx skills add <honeydeck-repo-url> --copy --skill honeydeck
26
+ npx skills add <honeydeck-repo-url> --copy --skill presentation-writing
27
+ npx skills add <honeydeck-repo-url> --copy --skill slidev-migration
28
+ ```
29
+
30
+ ## Bundled skills
31
+
32
+ | Skill | Use it for |
33
+ | --- | --- |
34
+ | `honeydeck` | Honeydeck-specific guidance for MDX decks, layouts, CSS imports, presenter notes, reveals, code steps, PDF export, and package docs. |
35
+ | `presentation-writing` | Framework-agnostic help with audience, storyline, slide headlines, speaker notes, timing, and review heuristics. |
36
+ | `slidev-migration` | Migrating Slidev decks to Honeydeck while preserving source files until you approve cleanup. |
37
+
38
+ The `honeydeck` and `presentation-writing` skills work well together: one keeps the agent inside Honeydeck conventions, while the other improves the story and delivery. Add `slidev-migration` when you are converting an existing Slidev project.
39
+
40
+ ## How agents use them
41
+
42
+ Skills do not change Honeydeck runtime behavior. They give your agent a focused local instruction file before it edits your deck.
43
+
44
+ For example, an agent with the `honeydeck` skill should prefer:
45
+
46
+ - `deck.mdx` as the entry file
47
+ - exact `---` slide separators
48
+ - slide frontmatter for layouts
49
+ - explicit imports from `@honeydeck/honeydeck`
50
+ - `<Notes>` for speaker notes
51
+ - `<Reveal>`, `<RevealGroup>`, and code metadata for steps
52
+ - `honeydeck pdf` for PDF export
53
+
54
+ The skills also point agents back to the packaged docs and specs, so generated deck changes can stay aligned with the installed Honeydeck version.
55
+
56
+ ## Updating skills
57
+
58
+ Skills are copied into the target scope when you install them. Re-run the installer after upgrading Honeydeck if you want the latest bundled instructions:
59
+
60
+ ```bash
61
+ npm update @honeydeck/honeydeck
62
+ honeydeck skill
63
+ ```
64
+
65
+ Coming from Slidev? See the [Slidev migration guide](slidev-migration.md) for the migration-specific workflow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honeydeck/honeydeck",
3
- "version": "0.2.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": {
package/skills/SPEC.md CHANGED
@@ -19,3 +19,4 @@ Expected behavior:
19
19
  - The `slidev-migration` skill instructs agents to inspect existing Slidev projects, preserve source files until the user approves cleanup, initialize or merge Honeydeck starter files, migrate common Slidev syntax to Honeydeck MDX/React, and document unsupported or approximated features.
20
20
  - `honeydeck init` should offer to open the interactive skills installer for the generated project and make clear that accepting runs `npx skills add`.
21
21
  - `honeydeck init` and `honeydeck skill` should delegate bundled skill selection, scope selection, and agent selection to the same `npx skills add <honeydeck-package-source> --copy` flow.
22
+ - `docs/skills.md` should document why and how to install the bundled skills, list the `honeydeck`, `presentation-writing`, and `slidev-migration` skills, and link to the Slidev migration guide for migration-specific details.
@@ -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
 
@@ -121,10 +121,11 @@ Reference page routes intentionally do not encode slide or step. During one brow
121
121
 
122
122
  Shown in normal slide view only (not presenter/reference views).
123
123
 
124
- - Positioned bottom-left
124
+ - Positioned bottom-center on narrow mobile screens and bottom-left from wider breakpoints
125
125
  - **Hidden by default** on desktop — appears on cursor hover near bottom edge
126
126
  - **Always visible** on mobile/tablet portrait
127
127
  - **Hidden by default** on mobile/tablet landscape — appears when the center tap zone is tapped, fades after roughly 3 seconds of idle time, and stays visible while being interacted with
128
+ - On narrow mobile screens, stays within the viewport by wrapping controls into compact groups instead of extending past the screen edge
128
129
  - Contains:
129
130
  - Current slide number
130
131
  - Navigation arrows (step left/right)
@@ -118,105 +118,117 @@ export function NavBar({
118
118
  const canPrev = getPreviousStepRoute(route, navigationOptions) !== null;
119
119
  const canNext = getNextStepRoute(route, navigationOptions) !== null;
120
120
  const FullscreenIcon = isFullscreen ? MinimizeIcon : MaximizeIcon;
121
+ const groupClass =
122
+ "flex items-center gap-0.5 rounded-md bg-white/[0.04] px-0.5 py-0.5 sm:bg-transparent sm:p-0";
121
123
 
122
124
  return (
123
125
  // Hover zone — transparent, occupies the bottom strip
124
126
  <div
125
- className="honeydeck-nav-zone fixed bottom-0 left-0 right-0 h-20 z-50 flex items-end pointer-events-none"
127
+ className="honeydeck-nav-zone fixed bottom-0 left-0 right-0 h-28 z-50 flex items-end justify-center pointer-events-none sm:h-20 sm:justify-start"
126
128
  data-honeydeck-no-swipe="true"
127
129
  >
128
130
  {/* Actual bar */}
129
- <div className="honeydeck-nav-bar pointer-events-auto flex items-center gap-1 px-2 py-1.5 ml-6 mb-6 bg-black/70 backdrop-blur rounded-lg border border-white/10 shadow-[0_4px_24px_rgba(0,0,0,0.4)]">
130
- {/* Prev step */}
131
- <NavBarButton
132
- onClick={goPrev}
133
- label="Previous step (←)"
134
- disabled={!canPrev}
135
- >
136
- <ChevronLeftIcon aria-hidden="true" size={16} />
137
- </NavBarButton>
138
-
139
- {/* Slide number */}
140
- <span className="min-w-6 px-1 text-center font-sans text-sm tabular-nums text-white/60">
141
- {route.slide}
142
- </span>
143
-
144
- {/* Next step */}
145
- <NavBarButton
146
- onClick={goNext}
147
- label="Next step (→)"
148
- disabled={!canNext}
149
- >
150
- <ChevronRightIcon aria-hidden="true" size={16} />
151
- </NavBarButton>
152
-
153
- <NavBarDivider />
154
-
155
- {/* Overview */}
156
- <NavBarButton
157
- onClick={onToggleOverview}
158
- label="Overview (o)"
159
- active={isOverview}
160
- >
161
- <LayoutGridIcon aria-hidden="true" size={14} />
162
- </NavBarButton>
163
-
164
- {/* Layouts reference */}
165
- <NavBarButton
166
- onClick={() => openReference(route)}
167
- label="Layouts reference"
168
- >
169
- <BookOpenTextIcon aria-hidden="true" size={16} />
170
- </NavBarButton>
171
-
172
- {/* Docs website */}
173
- <NavBarButton onClick={openDocsWebsite} label="Docs website">
174
- <ExternalLinkIcon aria-hidden="true" size={15} />
175
- </NavBarButton>
176
-
177
- {/* Presenter mode */}
178
- {route.view !== "presenter" && (
131
+ <div className="honeydeck-nav-bar pointer-events-auto mx-2 mb-2 flex max-w-[calc(100vw-1rem)] flex-wrap items-center justify-center gap-1 rounded-lg border border-white/10 bg-black/70 px-1.5 py-1.5 shadow-[0_4px_24px_rgba(0,0,0,0.4)] backdrop-blur sm:mx-0 sm:ml-6 sm:mb-6 sm:max-w-none sm:flex-nowrap sm:justify-start sm:gap-1 sm:px-2">
132
+ <div className={groupClass}>
133
+ {/* Prev step */}
179
134
  <NavBarButton
180
- onClick={() => openPresenter(route)}
181
- label="Presenter mode (p)"
135
+ onClick={goPrev}
136
+ label="Previous step ()"
137
+ disabled={!canPrev}
182
138
  >
183
- <PresentationIcon aria-hidden="true" size={16} />
139
+ <ChevronLeftIcon aria-hidden="true" size={16} />
184
140
  </NavBarButton>
185
- )}
186
141
 
187
- {/* Fullscreen */}
188
- <NavBarButton onClick={toggleFullscreen} label="Fullscreen (f)">
189
- <FullscreenIcon aria-hidden="true" size={16} />
190
- </NavBarButton>
142
+ {/* Slide number */}
143
+ <span className="min-w-6 px-1 text-center font-sans text-sm tabular-nums text-white/60">
144
+ {route.slide}
145
+ </span>
191
146
 
192
- {showTextSelectionToggle && onToggleTextSelection && (
147
+ {/* Next step */}
193
148
  <NavBarButton
194
- onClick={onToggleTextSelection}
195
- label={
196
- isTextSelectionEnabled
197
- ? "Disable slide text selection"
198
- : "Enable slide text selection"
199
- }
200
- active={isTextSelectionEnabled}
149
+ onClick={goNext}
150
+ label="Next step (→)"
151
+ disabled={!canNext}
201
152
  >
202
- <TextSelectIcon aria-hidden="true" size={16} />
153
+ <ChevronRightIcon aria-hidden="true" size={16} />
203
154
  </NavBarButton>
204
- )}
155
+ </div>
205
156
 
206
- <NavBarDivider />
157
+ <div className="hidden sm:block">
158
+ <NavBarDivider />
159
+ </div>
207
160
 
208
- {isZoomed && onResetZoom && (
209
- <NavBarButton onClick={onResetZoom} label="Reset zoom">
210
- <RotateCcwIcon aria-hidden="true" size={16} />
161
+ <div className={groupClass}>
162
+ {/* Overview */}
163
+ <NavBarButton
164
+ onClick={onToggleOverview}
165
+ label="Overview (o)"
166
+ active={isOverview}
167
+ >
168
+ <LayoutGridIcon aria-hidden="true" size={14} />
169
+ </NavBarButton>
170
+
171
+ {/* Layouts reference */}
172
+ <NavBarButton
173
+ onClick={() => openReference(route)}
174
+ label="Layouts reference"
175
+ >
176
+ <BookOpenTextIcon aria-hidden="true" size={16} />
211
177
  </NavBarButton>
212
- )}
213
-
214
- {/* Color mode */}
215
- <ColorModeCycleButton
216
- colorMode={colorMode}
217
- onSetColorMode={onSetColorMode}
218
- className={navBarButtonClass()}
219
- />
178
+
179
+ {/* Docs website */}
180
+ <NavBarButton onClick={openDocsWebsite} label="Docs website">
181
+ <ExternalLinkIcon aria-hidden="true" size={15} />
182
+ </NavBarButton>
183
+
184
+ {/* Presenter mode */}
185
+ {route.view !== "presenter" && (
186
+ <NavBarButton
187
+ onClick={() => openPresenter(route)}
188
+ label="Presenter mode (p)"
189
+ >
190
+ <PresentationIcon aria-hidden="true" size={16} />
191
+ </NavBarButton>
192
+ )}
193
+ </div>
194
+
195
+ <div className="hidden sm:block">
196
+ <NavBarDivider />
197
+ </div>
198
+
199
+ <div className={groupClass}>
200
+ {/* Fullscreen */}
201
+ <NavBarButton onClick={toggleFullscreen} label="Fullscreen (f)">
202
+ <FullscreenIcon aria-hidden="true" size={16} />
203
+ </NavBarButton>
204
+
205
+ {showTextSelectionToggle && onToggleTextSelection && (
206
+ <NavBarButton
207
+ onClick={onToggleTextSelection}
208
+ label={
209
+ isTextSelectionEnabled
210
+ ? "Disable slide text selection"
211
+ : "Enable slide text selection"
212
+ }
213
+ active={isTextSelectionEnabled}
214
+ >
215
+ <TextSelectIcon aria-hidden="true" size={16} />
216
+ </NavBarButton>
217
+ )}
218
+
219
+ {isZoomed && onResetZoom && (
220
+ <NavBarButton onClick={onResetZoom} label="Reset zoom">
221
+ <RotateCcwIcon aria-hidden="true" size={16} />
222
+ </NavBarButton>
223
+ )}
224
+
225
+ {/* Color mode */}
226
+ <ColorModeCycleButton
227
+ colorMode={colorMode}
228
+ onSetColorMode={onSetColorMode}
229
+ className={navBarButtonClass()}
230
+ />
231
+ </div>
220
232
  </div>
221
233
  </div>
222
234
  );
@@ -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(