@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 +1 -0
- package/docs/deeper-dive.md +2 -9
- package/docs/getting-started.md +1 -0
- package/docs/skills.md +65 -0
- package/package.json +1 -1
- package/skills/SPEC.md +1 -0
- package/src/layouts/SPEC.md +1 -1
- package/src/runtime/SPEC.md +2 -1
- package/src/runtime/components/NavBar.tsx +93 -81
- package/src/vite-plugin/layout-demo-crawler.ts +304 -33
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
|
package/docs/deeper-dive.md
CHANGED
|
@@ -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).
|
package/docs/getting-started.md
CHANGED
|
@@ -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
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.
|
package/src/layouts/SPEC.md
CHANGED
|
@@ -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
|
|
package/src/runtime/SPEC.md
CHANGED
|
@@ -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-
|
|
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
|
|
130
|
-
{
|
|
131
|
-
|
|
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={
|
|
181
|
-
label="
|
|
135
|
+
onClick={goPrev}
|
|
136
|
+
label="Previous step (←)"
|
|
137
|
+
disabled={!canPrev}
|
|
182
138
|
>
|
|
183
|
-
<
|
|
139
|
+
<ChevronLeftIcon aria-hidden="true" size={16} />
|
|
184
140
|
</NavBarButton>
|
|
185
|
-
)}
|
|
186
141
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
147
|
+
{/* Next step */}
|
|
193
148
|
<NavBarButton
|
|
194
|
-
onClick={
|
|
195
|
-
label=
|
|
196
|
-
|
|
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
|
-
<
|
|
153
|
+
<ChevronRightIcon aria-hidden="true" size={16} />
|
|
203
154
|
</NavBarButton>
|
|
204
|
-
|
|
155
|
+
</div>
|
|
205
156
|
|
|
206
|
-
<
|
|
157
|
+
<div className="hidden sm:block">
|
|
158
|
+
<NavBarDivider />
|
|
159
|
+
</div>
|
|
207
160
|
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
172
|
-
if (!layoutName || !localName) continue;
|
|
171
|
+
if (!layoutName) continue;
|
|
173
172
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
|
|
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(
|