@dfosco/storyboard 0.5.0-alpha.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/commandpalette.config.json +152 -0
- package/dist/storyboard-ui.css +1 -0
- package/dist/storyboard-ui.js +21328 -0
- package/dist/storyboard-ui.js.map +1 -0
- package/dist/tailwind.css +2 -0
- package/dist/tiny-canvas.css +1 -0
- package/dist/tiny-canvas.js +389 -0
- package/package.json +121 -0
- package/paste.config.json +67 -0
- package/scaffold/AGENTS.md +432 -0
- package/scaffold/agents/prompt-agent.agent.md +181 -0
- package/scaffold/agents/terminal-agent.agent.md +351 -0
- package/scaffold/codex/config.toml +246 -0
- package/scaffold/deploy.yml +103 -0
- package/scaffold/githooks/pre-push +114 -0
- package/scaffold/gitignore +64 -0
- package/scaffold/manifest.json +56 -0
- package/scaffold/preview.yml +181 -0
- package/scaffold/scripts/link.sh +26 -0
- package/scaffold/scripts/unlink.sh +10 -0
- package/scaffold/skills/agent-browser/SKILL.md +260 -0
- package/scaffold/skills/canvas/SKILL.md +364 -0
- package/scaffold/skills/create/SKILL.md +501 -0
- package/scaffold/skills/ship/SKILL.md +237 -0
- package/scaffold/skills/storyboard/SKILL.md +360 -0
- package/scaffold/skills/update-storyboard/SKILL.md +16 -0
- package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
- package/scaffold/skills/vitest/GENERATION.md +5 -0
- package/scaffold/skills/vitest/SKILL.md +52 -0
- package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
- package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
- package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
- package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
- package/scaffold/skills/vitest/references/core-cli.md +166 -0
- package/scaffold/skills/vitest/references/core-config.md +174 -0
- package/scaffold/skills/vitest/references/core-describe.md +193 -0
- package/scaffold/skills/vitest/references/core-expect.md +219 -0
- package/scaffold/skills/vitest/references/core-hooks.md +244 -0
- package/scaffold/skills/vitest/references/core-test-api.md +233 -0
- package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
- package/scaffold/skills/vitest/references/features-context.md +238 -0
- package/scaffold/skills/vitest/references/features-coverage.md +207 -0
- package/scaffold/skills/vitest/references/features-filtering.md +211 -0
- package/scaffold/skills/vitest/references/features-mocking.md +265 -0
- package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
- package/scaffold/skills/worktree/SKILL.md +93 -0
- package/scaffold/storyboard.config.json +44 -0
- package/src/canvas/Canvas.jsx +78 -0
- package/src/canvas/Draggable.jsx +235 -0
- package/src/canvas/index.d.ts +41 -0
- package/src/canvas/index.js +6 -0
- package/src/canvas/style.css +118 -0
- package/src/canvas/useResetCanvas.js +17 -0
- package/src/canvas/utils.js +136 -0
- package/src/core/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
- package/src/core/autosync/server.js +714 -0
- package/src/core/autosync/server.test.js +158 -0
- package/src/core/canvas/__tests__/agent-integration.test.js +596 -0
- package/src/core/canvas/__tests__/helpers/browser.js +95 -0
- package/src/core/canvas/__tests__/helpers/canvas-api.js +129 -0
- package/src/core/canvas/__tests__/helpers/perf.js +118 -0
- package/src/core/canvas/__tests__/helpers/setup.js +176 -0
- package/src/core/canvas/__tests__/helpers/tmux.js +130 -0
- package/src/core/canvas/__tests__/helpers/transcript.js +132 -0
- package/src/core/canvas/__tests__/terminal-integration.test.js +177 -0
- package/src/core/canvas/collision.js +292 -0
- package/src/core/canvas/collision.test.js +371 -0
- package/src/core/canvas/compact.js +83 -0
- package/src/core/canvas/deriveCanvasId.test.js +40 -0
- package/src/core/canvas/githubEmbeds.js +527 -0
- package/src/core/canvas/githubEmbeds.test.js +302 -0
- package/src/core/canvas/hot-pool.js +766 -0
- package/src/core/canvas/identity.js +107 -0
- package/src/core/canvas/identity.test.js +100 -0
- package/src/core/canvas/materializer.js +259 -0
- package/src/core/canvas/materializer.test.js +356 -0
- package/src/core/canvas/selectedWidgets.js +270 -0
- package/src/core/canvas/selectedWidgets.test.js +321 -0
- package/src/core/canvas/server.js +3134 -0
- package/src/core/canvas/server.test.js +379 -0
- package/src/core/canvas/terminal-config.js +330 -0
- package/src/core/canvas/terminal-registry.js +465 -0
- package/src/core/canvas/terminal-server.js +1436 -0
- package/src/core/canvas/writeGuard.js +53 -0
- package/src/core/cli/agent.js +85 -0
- package/src/core/cli/branch.js +386 -0
- package/src/core/cli/canvasAdd.js +241 -0
- package/src/core/cli/canvasBatch.js +98 -0
- package/src/core/cli/canvasBounds.js +160 -0
- package/src/core/cli/canvasRead.js +236 -0
- package/src/core/cli/canvasUpdate.js +179 -0
- package/src/core/cli/code.js +67 -0
- package/src/core/cli/compact.js +62 -0
- package/src/core/cli/create.js +674 -0
- package/src/core/cli/dev-helpers.js +53 -0
- package/src/core/cli/dev-helpers.test.js +53 -0
- package/src/core/cli/dev.js +430 -0
- package/src/core/cli/exit.js +38 -0
- package/src/core/cli/flags.js +174 -0
- package/src/core/cli/flags.test.js +155 -0
- package/src/core/cli/index.js +233 -0
- package/src/core/cli/intro.js +37 -0
- package/src/core/cli/proxy.js +319 -0
- package/src/core/cli/proxy.test.js +63 -0
- package/src/core/cli/schemas.js +223 -0
- package/src/core/cli/server.js +192 -0
- package/src/core/cli/serverUrl.js +61 -0
- package/src/core/cli/sessions.js +459 -0
- package/src/core/cli/setup.js +404 -0
- package/src/core/cli/terminal-commands.js +287 -0
- package/src/core/cli/terminal-messaging.js +231 -0
- package/src/core/cli/terminal-welcome.js +515 -0
- package/src/core/cli/updateVersion.js +124 -0
- package/src/core/comments/api.js +284 -0
- package/src/core/comments/api.test.js +282 -0
- package/src/core/comments/auth.js +151 -0
- package/src/core/comments/auth.test.js +167 -0
- package/src/core/comments/commentCache.js +109 -0
- package/src/core/comments/commentCache.test.js +48 -0
- package/src/core/comments/commentDrafts.js +68 -0
- package/src/core/comments/commentMode.js +63 -0
- package/src/core/comments/commentMode.test.js +90 -0
- package/src/core/comments/config.js +47 -0
- package/src/core/comments/config.test.js +77 -0
- package/src/core/comments/graphql.js +65 -0
- package/src/core/comments/graphql.test.js +95 -0
- package/src/core/comments/index.js +42 -0
- package/src/core/comments/metadata.js +52 -0
- package/src/core/comments/metadata.test.js +110 -0
- package/src/core/comments/queries.js +245 -0
- package/src/core/comments/ui/AuthModal.jsx +114 -0
- package/src/core/comments/ui/CommentOverlay.js +52 -0
- package/src/core/comments/ui/CommentWindow.jsx +329 -0
- package/src/core/comments/ui/CommentsDrawer.jsx +102 -0
- package/src/core/comments/ui/Composer.jsx +64 -0
- package/src/core/comments/ui/authModal.js +66 -0
- package/src/core/comments/ui/authModal.test.js +76 -0
- package/src/core/comments/ui/comment-cursor-dark.svg +1 -0
- package/src/core/comments/ui/comment-cursor.svg +1 -0
- package/src/core/comments/ui/comment-layout.css +142 -0
- package/src/core/comments/ui/commentWindow.js +121 -0
- package/src/core/comments/ui/comments.css +242 -0
- package/src/core/comments/ui/commentsDrawer.js +84 -0
- package/src/core/comments/ui/composer.js +136 -0
- package/src/core/comments/ui/index.js +14 -0
- package/src/core/comments/ui/mount.js +687 -0
- package/src/core/comments/ui/mount.test.js +336 -0
- package/src/core/data/dotPath.js +53 -0
- package/src/core/data/dotPath.test.js +114 -0
- package/src/core/data/loader.js +409 -0
- package/src/core/data/loader.test.js +599 -0
- package/src/core/data/viewfinder.js +363 -0
- package/src/core/data/viewfinder.test.js +456 -0
- package/src/core/devtools/devtools-consumer.js +28 -0
- package/src/core/devtools/devtools.js +144 -0
- package/src/core/devtools/devtools.test.js +75 -0
- package/src/core/devtools/sceneDebug.js +112 -0
- package/src/core/devtools/sceneDebug.test.js +141 -0
- package/src/core/index.js +124 -0
- package/src/core/inspector/fiberWalker.js +239 -0
- package/src/core/inspector/highlighter.js +275 -0
- package/src/core/inspector/mouseMode.js +259 -0
- package/src/core/lib/components/ui/alert/alert-action.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert-description.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert-title.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert.jsx +25 -0
- package/src/core/lib/components/ui/alert/index.js +17 -0
- package/src/core/lib/components/ui/avatar/avatar-badge.jsx +22 -0
- package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
- package/src/core/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
- package/src/core/lib/components/ui/avatar/avatar-group.jsx +19 -0
- package/src/core/lib/components/ui/avatar/avatar-image.jsx +15 -0
- package/src/core/lib/components/ui/avatar/avatar.jsx +19 -0
- package/src/core/lib/components/ui/avatar/index.js +22 -0
- package/src/core/lib/components/ui/badge/badge.jsx +31 -0
- package/src/core/lib/components/ui/badge/index.js +2 -0
- package/src/core/lib/components/ui/button/button.jsx +100 -0
- package/src/core/lib/components/ui/button/index.js +12 -0
- package/src/core/lib/components/ui/card/card-action.jsx +11 -0
- package/src/core/lib/components/ui/card/card-content.jsx +11 -0
- package/src/core/lib/components/ui/card/card-description.jsx +11 -0
- package/src/core/lib/components/ui/card/card-footer.jsx +11 -0
- package/src/core/lib/components/ui/card/card-header.jsx +19 -0
- package/src/core/lib/components/ui/card/card-title.jsx +11 -0
- package/src/core/lib/components/ui/card/card.jsx +17 -0
- package/src/core/lib/components/ui/card/index.js +25 -0
- package/src/core/lib/components/ui/checkbox/checkbox.jsx +29 -0
- package/src/core/lib/components/ui/checkbox/index.js +6 -0
- package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/collapsible.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/index.js +13 -0
- package/src/core/lib/components/ui/dialog/dialog-close.jsx +7 -0
- package/src/core/lib/components/ui/dialog/dialog-content.jsx +34 -0
- package/src/core/lib/components/ui/dialog/dialog-description.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-footer.jsx +23 -0
- package/src/core/lib/components/ui/dialog/dialog-header.jsx +11 -0
- package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-portal.jsx +4 -0
- package/src/core/lib/components/ui/dialog/dialog-title.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
- package/src/core/lib/components/ui/dialog/dialog.jsx +4 -0
- package/src/core/lib/components/ui/dialog/index.js +34 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/index.js +54 -0
- package/src/core/lib/components/ui/input/index.js +7 -0
- package/src/core/lib/components/ui/input/input.jsx +19 -0
- package/src/core/lib/components/ui/label/index.js +7 -0
- package/src/core/lib/components/ui/label/label.jsx +19 -0
- package/src/core/lib/components/ui/panel/index.js +24 -0
- package/src/core/lib/components/ui/panel/panel-body.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-close.jsx +16 -0
- package/src/core/lib/components/ui/panel/panel-content.jsx +29 -0
- package/src/core/lib/components/ui/panel/panel-footer.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-header.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-title.jsx +12 -0
- package/src/core/lib/components/ui/panel/panel.jsx +4 -0
- package/src/core/lib/components/ui/popover/index.js +28 -0
- package/src/core/lib/components/ui/popover/popover-close.jsx +7 -0
- package/src/core/lib/components/ui/popover/popover-content.jsx +22 -0
- package/src/core/lib/components/ui/popover/popover-description.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-header.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-portal.jsx +4 -0
- package/src/core/lib/components/ui/popover/popover-title.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-trigger.jsx +8 -0
- package/src/core/lib/components/ui/popover/popover.jsx +4 -0
- package/src/core/lib/components/ui/searchable-list.jsx +160 -0
- package/src/core/lib/components/ui/select/index.js +37 -0
- package/src/core/lib/components/ui/select/select-content.jsx +30 -0
- package/src/core/lib/components/ui/select/select-group-heading.jsx +17 -0
- package/src/core/lib/components/ui/select/select-group.jsx +15 -0
- package/src/core/lib/components/ui/select/select-item.jsx +26 -0
- package/src/core/lib/components/ui/select/select-label.jsx +11 -0
- package/src/core/lib/components/ui/select/select-portal.jsx +4 -0
- package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
- package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
- package/src/core/lib/components/ui/select/select-separator.jsx +15 -0
- package/src/core/lib/components/ui/select/select-trigger.jsx +25 -0
- package/src/core/lib/components/ui/select/select.jsx +4 -0
- package/src/core/lib/components/ui/separator/index.js +7 -0
- package/src/core/lib/components/ui/separator/separator.jsx +22 -0
- package/src/core/lib/components/ui/sheet/index.js +34 -0
- package/src/core/lib/components/ui/sheet/sheet-close.jsx +7 -0
- package/src/core/lib/components/ui/sheet/sheet-content.jsx +35 -0
- package/src/core/lib/components/ui/sheet/sheet-description.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-footer.jsx +11 -0
- package/src/core/lib/components/ui/sheet/sheet-header.jsx +11 -0
- package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-portal.jsx +4 -0
- package/src/core/lib/components/ui/sheet/sheet-title.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
- package/src/core/lib/components/ui/sheet/sheet.jsx +4 -0
- package/src/core/lib/components/ui/textarea/index.js +7 -0
- package/src/core/lib/components/ui/textarea/textarea.jsx +18 -0
- package/src/core/lib/components/ui/toggle/index.js +8 -0
- package/src/core/lib/components/ui/toggle/toggle.jsx +36 -0
- package/src/core/lib/components/ui/toggle-group/index.js +10 -0
- package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
- package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
- package/src/core/lib/components/ui/tooltip/index.js +3 -0
- package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
- package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
- package/src/core/lib/components/ui/tooltip/tooltip.jsx +11 -0
- package/src/core/lib/components/ui/trigger-button/index.js +6 -0
- package/src/core/lib/components/ui/trigger-button/trigger-button.css +38 -0
- package/src/core/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
- package/src/core/lib/utils/index.js +6 -0
- package/src/core/logger/devLogger.js +238 -0
- package/src/core/logger/devLogger.test.js +193 -0
- package/src/core/modes/modes.css +98 -0
- package/src/core/modes/modes.js +492 -0
- package/src/core/modes/modes.test.js +562 -0
- package/src/core/mountStoryboardCore.js +478 -0
- package/src/core/rename-watcher/config.json +23 -0
- package/src/core/rename-watcher/watcher.js +531 -0
- package/src/core/scaffold.js +100 -0
- package/src/core/server/index.js +391 -0
- package/src/core/session/bodyClasses.js +128 -0
- package/src/core/session/bodyClasses.test.js +192 -0
- package/src/core/session/hashSubscribe.js +19 -0
- package/src/core/session/hashSubscribe.test.js +62 -0
- package/src/core/session/hideMode.js +424 -0
- package/src/core/session/hideMode.test.js +268 -0
- package/src/core/session/interceptHideParams.js +35 -0
- package/src/core/session/interceptHideParams.test.js +90 -0
- package/src/core/session/localStorage.js +134 -0
- package/src/core/session/localStorage.test.js +148 -0
- package/src/core/session/session.js +76 -0
- package/src/core/session/session.test.js +91 -0
- package/src/core/stores/canvasConfig.js +134 -0
- package/src/core/stores/canvasConfig.test.js +120 -0
- package/src/core/stores/commandActions.js +284 -0
- package/src/core/stores/commandPaletteConfig.js +31 -0
- package/src/core/stores/configSchema.js +232 -0
- package/src/core/stores/configSchema.test.js +72 -0
- package/src/core/stores/configStore.js +161 -0
- package/src/core/stores/customerModeConfig.js +30 -0
- package/src/core/stores/featureFlags.js +127 -0
- package/src/core/stores/paletteProviders.js +360 -0
- package/src/core/stores/paletteProviders.test.js +186 -0
- package/src/core/stores/plugins.js +40 -0
- package/src/core/stores/plugins.test.js +68 -0
- package/src/core/stores/recentArtifacts.js +68 -0
- package/src/core/stores/recentArtifacts.test.js +71 -0
- package/src/core/stores/sidePanelStore.ts +143 -0
- package/src/core/stores/themeStore.ts +291 -0
- package/src/core/stores/toolRegistry.js +227 -0
- package/src/core/stores/toolStateStore.js +183 -0
- package/src/core/stores/toolStateStore.test.js +220 -0
- package/src/core/stores/toolbarConfigStore.js +165 -0
- package/src/core/stores/uiConfig.js +64 -0
- package/src/core/stores/uiConfig.test.js +63 -0
- package/src/core/styles/tailwind.css +204 -0
- package/src/core/tools/handlers/autosync.js +12 -0
- package/src/core/tools/handlers/canvasAddWidget.js +11 -0
- package/src/core/tools/handlers/canvasAgents.js +20 -0
- package/src/core/tools/handlers/canvasToolbar.js +56 -0
- package/src/core/tools/handlers/commandPalette.js +9 -0
- package/src/core/tools/handlers/comments.js +16 -0
- package/src/core/tools/handlers/create.js +39 -0
- package/src/core/tools/handlers/devtools.js +122 -0
- package/src/core/tools/handlers/devtools.test.js +87 -0
- package/src/core/tools/handlers/featureFlags.js +21 -0
- package/src/core/tools/handlers/flows.js +68 -0
- package/src/core/tools/handlers/hideChrome.js +9 -0
- package/src/core/tools/handlers/hideToolbars.js +25 -0
- package/src/core/tools/handlers/inspector.js +19 -0
- package/src/core/tools/handlers/paletteTheme.js +35 -0
- package/src/core/tools/handlers/theme.js +9 -0
- package/src/core/tools/registry.js +26 -0
- package/src/core/tools/surfaces/canvasToolbar.js +10 -0
- package/src/core/tools/surfaces/commandList.js +10 -0
- package/src/core/tools/surfaces/mainToolbar.js +11 -0
- package/src/core/tools/surfaces/registry.js +19 -0
- package/src/core/ui/ActionMenuButton.jsx +114 -0
- package/src/core/ui/AutosyncMenuButton.css +67 -0
- package/src/core/ui/AutosyncMenuButton.jsx +242 -0
- package/src/core/ui/BranchSelect.jsx +29 -0
- package/src/core/ui/BranchSelect.module.css +30 -0
- package/src/core/ui/CanvasAgentsMenu.jsx +89 -0
- package/src/core/ui/CanvasCreateMenu.jsx +611 -0
- package/src/core/ui/CanvasSnap.css +27 -0
- package/src/core/ui/CanvasSnap.jsx +51 -0
- package/src/core/ui/CanvasUndoRedo.css +36 -0
- package/src/core/ui/CanvasUndoRedo.jsx +62 -0
- package/src/core/ui/CanvasZoomControl.css +53 -0
- package/src/core/ui/CanvasZoomControl.jsx +49 -0
- package/src/core/ui/CanvasZoomToFit.css +18 -0
- package/src/core/ui/CanvasZoomToFit.jsx +26 -0
- package/src/core/ui/CommandMenu.css +8 -0
- package/src/core/ui/CommandMenu.jsx +287 -0
- package/src/core/ui/CommandPalette.jsx +35 -0
- package/src/core/ui/CommandPaletteTrigger.jsx +25 -0
- package/src/core/ui/CommentsMenuButton.jsx +40 -0
- package/src/core/ui/CoreUIBar.css +47 -0
- package/src/core/ui/CoreUIBar.jsx +905 -0
- package/src/core/ui/CreateMenuButton.jsx +117 -0
- package/src/core/ui/HideChromeTrigger.jsx +48 -0
- package/src/core/ui/Icon.jsx +279 -0
- package/src/core/ui/InspectorPanel.css +109 -0
- package/src/core/ui/InspectorPanel.jsx +632 -0
- package/src/core/ui/PwaInstallBanner.css +42 -0
- package/src/core/ui/PwaInstallBanner.jsx +124 -0
- package/src/core/ui/SidePanel.jsx +261 -0
- package/src/core/ui/ThemeMenuButton.jsx +139 -0
- package/src/core/ui/core-ui-colors.css +129 -0
- package/src/core/ui/design-modes.ts +7 -0
- package/src/core/ui/sidepanel.css +301 -0
- package/src/core/ui/viewfinder.ts +7 -0
- package/src/core/ui-entry.js +30 -0
- package/src/core/utils/fuzzySearch.js +117 -0
- package/src/core/utils/fuzzySearch.test.js +119 -0
- package/src/core/utils/mobileViewport.js +57 -0
- package/src/core/utils/mobileViewport.test.js +68 -0
- package/src/core/utils/prodMode.js +38 -0
- package/src/core/utils/smoothCorners.js +20 -0
- package/src/core/vite/docs-handler.js +155 -0
- package/src/core/vite/server-plugin.js +797 -0
- package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
- package/src/core/workshop/features/createCanvas/index.js +14 -0
- package/src/core/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
- package/src/core/workshop/features/createFlow/index.js +19 -0
- package/src/core/workshop/features/createFlow/server.js +663 -0
- package/src/core/workshop/features/createPage/CreatePageForm.jsx +304 -0
- package/src/core/workshop/features/createPage/index.js +11 -0
- package/src/core/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
- package/src/core/workshop/features/createPrototype/index.js +19 -0
- package/src/core/workshop/features/createPrototype/server.js +433 -0
- package/src/core/workshop/features/createStory/CreateStoryForm.jsx +208 -0
- package/src/core/workshop/features/createStory/index.js +14 -0
- package/src/core/workshop/features/registry-server.js +22 -0
- package/src/core/workshop/features/registry.js +28 -0
- package/src/core/workshop/features/templateIndex.js +155 -0
- package/src/core/workshop/ui/WorkshopPanel.jsx +98 -0
- package/src/core/workshop/ui/mount.ts +6 -0
- package/src/core/worktree/port.js +268 -0
- package/src/core/worktree/port.test.js +222 -0
- package/src/core/worktree/serverRegistry.js +120 -0
- package/src/internals/AuthModal/AuthModal.jsx +132 -0
- package/src/internals/AuthModal/AuthModal.module.css +221 -0
- package/src/internals/BranchBar/BranchBar.jsx +87 -0
- package/src/internals/BranchBar/BranchBar.module.css +247 -0
- package/src/internals/BranchBar/useBranches.js +93 -0
- package/src/internals/BranchBar/useBranches.test.js +68 -0
- package/src/internals/CommandPalette/CommandPalette.jsx +1361 -0
- package/src/internals/CommandPalette/CreateDialog.jsx +219 -0
- package/src/internals/CommandPalette/command-palette.css +180 -0
- package/src/internals/FlowError.module.css +30 -0
- package/src/internals/Icon.jsx +279 -0
- package/src/internals/StoryboardContext.js +3 -0
- package/src/internals/Viewfinder.jsx +1479 -0
- package/src/internals/Viewfinder.module.css +1540 -0
- package/src/internals/Workspace.jsx +7 -0
- package/src/internals/__mocks__/virtual-storyboard-data-index.js +4 -0
- package/src/internals/canvas/CanvasControls.jsx +112 -0
- package/src/internals/canvas/CanvasControls.module.css +135 -0
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +387 -0
- package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +350 -0
- package/src/internals/canvas/CanvasPage.jsx +3092 -0
- package/src/internals/canvas/CanvasPage.module.css +187 -0
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +358 -0
- package/src/internals/canvas/CanvasToolbar.jsx +73 -0
- package/src/internals/canvas/CanvasToolbar.module.css +92 -0
- package/src/internals/canvas/ComponentErrorBoundary.jsx +50 -0
- package/src/internals/canvas/ConnectorLayer.jsx +208 -0
- package/src/internals/canvas/ConnectorLayer.module.css +129 -0
- package/src/internals/canvas/MarqueeOverlay.jsx +20 -0
- package/src/internals/canvas/PageSelector.jsx +587 -0
- package/src/internals/canvas/PageSelector.module.css +261 -0
- package/src/internals/canvas/PageSelector.test.jsx +113 -0
- package/src/internals/canvas/WebGLContextPool.jsx +292 -0
- package/src/internals/canvas/WebGLContextPool.test.jsx +165 -0
- package/src/internals/canvas/canvasApi.js +164 -0
- package/src/internals/canvas/canvasReloadGuard.js +37 -0
- package/src/internals/canvas/canvasReloadGuard.test.js +27 -0
- package/src/internals/canvas/canvasTheme.js +118 -0
- package/src/internals/canvas/componentIsolate.jsx +165 -0
- package/src/internals/canvas/componentSetIsolate.jsx +257 -0
- package/src/internals/canvas/computeCanvasBounds.test.js +121 -0
- package/src/internals/canvas/connectorGeometry.js +132 -0
- package/src/internals/canvas/hotPoolDevLogs.js +25 -0
- package/src/internals/canvas/textSelection.js +10 -0
- package/src/internals/canvas/textSelection.test.js +26 -0
- package/src/internals/canvas/useCanvas.js +126 -0
- package/src/internals/canvas/useCanvas.test.js +26 -0
- package/src/internals/canvas/useMarqueeSelect.js +213 -0
- package/src/internals/canvas/useMarqueeSelect.test.js +78 -0
- package/src/internals/canvas/useUndoRedo.js +86 -0
- package/src/internals/canvas/useUndoRedo.test.js +231 -0
- package/src/internals/canvas/widgets/CodePenEmbed.jsx +293 -0
- package/src/internals/canvas/widgets/CodePenEmbed.module.css +161 -0
- package/src/internals/canvas/widgets/ComponentSetWidget.jsx +2 -0
- package/src/internals/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/internals/canvas/widgets/ComponentWidget.jsx +14 -0
- package/src/internals/canvas/widgets/ComponentWidget.module.css +0 -0
- package/src/internals/canvas/widgets/CropOverlay.jsx +179 -0
- package/src/internals/canvas/widgets/CropOverlay.module.css +154 -0
- package/src/internals/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/internals/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/internals/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/internals/canvas/widgets/FigmaEmbed.jsx +296 -0
- package/src/internals/canvas/widgets/FigmaEmbed.module.css +222 -0
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +151 -0
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +83 -0
- package/src/internals/canvas/widgets/ImageWidget.jsx +287 -0
- package/src/internals/canvas/widgets/ImageWidget.module.css +81 -0
- package/src/internals/canvas/widgets/LinkPreview.jsx +439 -0
- package/src/internals/canvas/widgets/LinkPreview.module.css +585 -0
- package/src/internals/canvas/widgets/LinkPreview.test.jsx +193 -0
- package/src/internals/canvas/widgets/MarkdownBlock.jsx +354 -0
- package/src/internals/canvas/widgets/MarkdownBlock.module.css +377 -0
- package/src/internals/canvas/widgets/MarkdownBlock.test.jsx +92 -0
- package/src/internals/canvas/widgets/PromptWidget.jsx +428 -0
- package/src/internals/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +463 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.module.css +579 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
- package/src/internals/canvas/widgets/ResizeHandle.jsx +67 -0
- package/src/internals/canvas/widgets/ResizeHandle.module.css +29 -0
- package/src/internals/canvas/widgets/StickyNote.jsx +92 -0
- package/src/internals/canvas/widgets/StickyNote.module.css +70 -0
- package/src/internals/canvas/widgets/StickyNote.test.jsx +116 -0
- package/src/internals/canvas/widgets/StorySetWidget.jsx +208 -0
- package/src/internals/canvas/widgets/StorySetWidget.module.css +89 -0
- package/src/internals/canvas/widgets/StoryWidget.jsx +334 -0
- package/src/internals/canvas/widgets/StoryWidget.module.css +211 -0
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/internals/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/internals/canvas/widgets/TerminalWidget.jsx +704 -0
- package/src/internals/canvas/widgets/TerminalWidget.module.css +444 -0
- package/src/internals/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/internals/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/internals/canvas/widgets/WidgetChrome.jsx +580 -0
- package/src/internals/canvas/widgets/WidgetChrome.module.css +421 -0
- package/src/internals/canvas/widgets/WidgetWrapper.jsx +15 -0
- package/src/internals/canvas/widgets/WidgetWrapper.module.css +25 -0
- package/src/internals/canvas/widgets/codepenUrl.js +75 -0
- package/src/internals/canvas/widgets/codepenUrl.test.js +76 -0
- package/src/internals/canvas/widgets/embedInteraction.test.jsx +173 -0
- package/src/internals/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/internals/canvas/widgets/embedTheme.js +148 -0
- package/src/internals/canvas/widgets/expandUtils.js +559 -0
- package/src/internals/canvas/widgets/expandUtils.test.js +155 -0
- package/src/internals/canvas/widgets/figmaUrl.js +118 -0
- package/src/internals/canvas/widgets/figmaUrl.test.js +139 -0
- package/src/internals/canvas/widgets/githubUrl.js +82 -0
- package/src/internals/canvas/widgets/githubUrl.test.js +74 -0
- package/src/internals/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/internals/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/internals/canvas/widgets/index.js +42 -0
- package/src/internals/canvas/widgets/pasteRules.js +295 -0
- package/src/internals/canvas/widgets/pasteRules.test.js +474 -0
- package/src/internals/canvas/widgets/snapshotDisplay.test.jsx +211 -0
- package/src/internals/canvas/widgets/tilePool.js +23 -0
- package/src/internals/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/internals/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/internals/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/internals/canvas/widgets/tiles/leaf.png +0 -0
- package/src/internals/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/internals/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/internals/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/internals/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/internals/canvas/widgets/widgetConfig.js +291 -0
- package/src/internals/canvas/widgets/widgetConfig.test.js +68 -0
- package/src/internals/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/internals/canvas/widgets/widgetProps.js +133 -0
- package/src/internals/context/FormContext.js +13 -0
- package/src/internals/context/FormContext.test.js +48 -0
- package/src/internals/context.jsx +481 -0
- package/src/internals/context.test.jsx +296 -0
- package/src/internals/hashPreserver.js +73 -0
- package/src/internals/hashPreserver.test.js +107 -0
- package/src/internals/hooks/useConfig.js +14 -0
- package/src/internals/hooks/useFeatureFlag.js +14 -0
- package/src/internals/hooks/useFlows.js +50 -0
- package/src/internals/hooks/useFlows.test.js +134 -0
- package/src/internals/hooks/useHideMode.js +31 -0
- package/src/internals/hooks/useHideMode.test.js +43 -0
- package/src/internals/hooks/useLocalStorage.js +57 -0
- package/src/internals/hooks/useLocalStorage.test.js +75 -0
- package/src/internals/hooks/useMode.js +43 -0
- package/src/internals/hooks/useObject.js +101 -0
- package/src/internals/hooks/useObject.test.js +74 -0
- package/src/internals/hooks/useOverride.js +84 -0
- package/src/internals/hooks/useOverride.test.js +71 -0
- package/src/internals/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/internals/hooks/useRecord.js +158 -0
- package/src/internals/hooks/useRecord.test.js +221 -0
- package/src/internals/hooks/useScene.js +38 -0
- package/src/internals/hooks/useScene.test.js +66 -0
- package/src/internals/hooks/useSceneData.js +108 -0
- package/src/internals/hooks/useSceneData.test.js +136 -0
- package/src/internals/hooks/useSession.js +4 -0
- package/src/internals/hooks/useSession.test.js +8 -0
- package/src/internals/hooks/useThemeState.js +61 -0
- package/src/internals/hooks/useThemeState.test.js +66 -0
- package/src/internals/hooks/useUndoRedo.js +28 -0
- package/src/internals/hooks/useUndoRedo.test.js +64 -0
- package/src/internals/index.js +58 -0
- package/src/internals/story/ComponentSetPage.jsx +198 -0
- package/src/internals/story/ComponentSetPage.module.css +129 -0
- package/src/internals/story/StoryPage.jsx +147 -0
- package/src/internals/story/StoryPage.module.css +18 -0
- package/src/internals/test-utils.js +45 -0
- package/src/internals/vite/data-plugin.js +1508 -0
- package/src/internals/vite/data-plugin.test.js +1223 -0
- package/src/test-utils.js +44 -0
- package/toolbar.config.json +271 -0
- package/widgets.config.json +1537 -0
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
4
|
+
import { globSync } from 'glob'
|
|
5
|
+
import { parse as parseJsonc } from 'jsonc-parser'
|
|
6
|
+
import { materializeFromText } from '../../core/canvas/materializer.js'
|
|
7
|
+
import { toCanvasId } from '../../core/canvas/identity.js'
|
|
8
|
+
import { isCanvasWriteInFlight } from '../../core/canvas/writeGuard.js'
|
|
9
|
+
import { getConfig } from '../../core/stores/configSchema.js'
|
|
10
|
+
import { list as listRunningServers } from '../../core/worktree/serverRegistry.js'
|
|
11
|
+
|
|
12
|
+
const VIRTUAL_MODULE_ID = 'virtual:storyboard-data-index'
|
|
13
|
+
const RESOLVED_ID = '\0' + VIRTUAL_MODULE_ID
|
|
14
|
+
|
|
15
|
+
const GLOB_PATTERN = '**/*.{flow,scene,object,record,prototype,folder}.{json,jsonc}'
|
|
16
|
+
const CANVAS_GLOB_PATTERN = '**/*.canvas.jsonl'
|
|
17
|
+
const CANVAS_META_GLOB_PATTERN = '**/*.meta.json'
|
|
18
|
+
const STORY_GLOB_PATTERN = '**/*.story.{jsx,tsx}'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract the data name and type suffix from a file path.
|
|
22
|
+
* Flows, records, and objects inside src/prototypes/{Name}/ get prefixed with
|
|
23
|
+
* the prototype name (e.g. "Dashboard/default", "Dashboard/helpers").
|
|
24
|
+
* Directories ending in .folder/ are skipped when extracting prototype scope.
|
|
25
|
+
*
|
|
26
|
+
* e.g. "src/data/default.flow.json" → { name: "default", suffix: "flow" }
|
|
27
|
+
* "src/prototypes/Dashboard/default.flow.json" → { name: "Dashboard/default", suffix: "flow" }
|
|
28
|
+
* "src/prototypes/Dashboard/helpers.object.json"→ { name: "Dashboard/helpers", suffix: "object" }
|
|
29
|
+
* "src/prototypes/X.folder/Dashboard/default.flow.json" → { name: "Dashboard/default", suffix: "flow", folder: "X" }
|
|
30
|
+
*/
|
|
31
|
+
function parseDataFile(filePath) {
|
|
32
|
+
const base = path.basename(filePath)
|
|
33
|
+
|
|
34
|
+
// Handle .canvas.jsonl files
|
|
35
|
+
const canvasJsonlMatch = base.match(/^(.+)\.canvas\.jsonl$/)
|
|
36
|
+
if (canvasJsonlMatch) {
|
|
37
|
+
if (canvasJsonlMatch[1].startsWith('_')) return null
|
|
38
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
39
|
+
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
40
|
+
|
|
41
|
+
const baseName = canvasJsonlMatch[1]
|
|
42
|
+
let name = baseName
|
|
43
|
+
let inferredRoute = null
|
|
44
|
+
const canvasFolderMatch = normalized.match(/(?:^|\/)src\/canvas\/([^/]+)\.folder\//)
|
|
45
|
+
const canvasFolderName = canvasFolderMatch ? canvasFolderMatch[1] : null
|
|
46
|
+
const folderDirMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\.folder\//)
|
|
47
|
+
const folderName = folderDirMatch ? folderDirMatch[1] : null
|
|
48
|
+
|
|
49
|
+
const canvasCheck = normalized.match(/(?:^|\/)src\/canvas\//)
|
|
50
|
+
if (canvasCheck) {
|
|
51
|
+
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
52
|
+
// Path-based ID: include folder context for uniqueness.
|
|
53
|
+
// .folder dirs contribute their name (sans .folder suffix) to the ID.
|
|
54
|
+
const idBase = (dirPath + '/')
|
|
55
|
+
.replace(/^.*?src\/canvas\//, '')
|
|
56
|
+
.replace(/\.folder\/?/g, '/')
|
|
57
|
+
.replace(/\/+/g, '/')
|
|
58
|
+
.replace(/\/$/, '')
|
|
59
|
+
name = idBase ? `${idBase}/${baseName}` : baseName
|
|
60
|
+
inferredRoute = '/canvas/' + name
|
|
61
|
+
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
|
|
62
|
+
}
|
|
63
|
+
const protoCheck = normalized.match(/(?:^|\/)src\/prototypes\//)
|
|
64
|
+
if (!canvasCheck && protoCheck) {
|
|
65
|
+
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
66
|
+
// For prototypes, .folder is purely organizational — strip entirely
|
|
67
|
+
const idBase = (dirPath + '/')
|
|
68
|
+
.replace(/^.*?src\/prototypes\//, '')
|
|
69
|
+
.replace(/[^/]*\.folder\/?/g, '')
|
|
70
|
+
.replace(/\/+/g, '/')
|
|
71
|
+
.replace(/\/$/, '')
|
|
72
|
+
name = idBase ? `${idBase}/${baseName}` : baseName
|
|
73
|
+
inferredRoute = '/canvas/' + name
|
|
74
|
+
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
|
|
75
|
+
}
|
|
76
|
+
// Derive group: canvases sharing a directory form a group
|
|
77
|
+
const slashIdx = name.lastIndexOf('/')
|
|
78
|
+
const group = canvasFolderName || (slashIdx > 0 ? name.substring(0, slashIdx) : null)
|
|
79
|
+
// Extract a relative path for toCanvasId (it expects src/canvas/... or src/prototypes/...)
|
|
80
|
+
const canvasIdInput = normalized.replace(/^.*?(src\/(?:canvas|prototypes)\/)/, '$1')
|
|
81
|
+
return { name, suffix: 'canvas', ext: 'jsonl', folder: canvasFolderName || folderName, inferredRoute, id: toCanvasId(canvasIdInput), group }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle canvas .meta.json files
|
|
85
|
+
const metaMatch = base.match(/^(.+)\.meta\.json$/)
|
|
86
|
+
if (metaMatch) {
|
|
87
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
88
|
+
// Only handle meta files inside src/canvas/ directories
|
|
89
|
+
const canvasCheck = normalized.match(/(?:^|\/)src\/canvas\//)
|
|
90
|
+
if (!canvasCheck) return null
|
|
91
|
+
// Skip _-prefixed
|
|
92
|
+
if (metaMatch[1].startsWith('_')) return null
|
|
93
|
+
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
94
|
+
return { name: metaMatch[1], suffix: 'canvas-meta', ext: 'json', inferredRoute: null }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle .story.jsx / .story.tsx files
|
|
98
|
+
const storyMatch = base.match(/^(.+)\.story\.(jsx|tsx)$/)
|
|
99
|
+
if (storyMatch) {
|
|
100
|
+
if (storyMatch[1].startsWith('_')) return null
|
|
101
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
102
|
+
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
103
|
+
|
|
104
|
+
const name = storyMatch[1]
|
|
105
|
+
let inferredRoute = null
|
|
106
|
+
|
|
107
|
+
// All stories route under /components/ regardless of directory location
|
|
108
|
+
const canvasCheck = normalized.match(/(?:^|\/)src\/canvas\//)
|
|
109
|
+
const componentsCheck = normalized.match(/(?:^|\/)src\/components\//)
|
|
110
|
+
if (canvasCheck) {
|
|
111
|
+
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
112
|
+
const routeBase = (dirPath + '/')
|
|
113
|
+
.replace(/^.*?src\/canvas\//, '')
|
|
114
|
+
.replace(/[^/]*\.folder\/?/g, '')
|
|
115
|
+
.replace(/\/$/, '')
|
|
116
|
+
inferredRoute = '/components/' + (routeBase ? routeBase + '/' : '') + name
|
|
117
|
+
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/components'
|
|
118
|
+
} else if (componentsCheck) {
|
|
119
|
+
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
120
|
+
const routeBase = (dirPath + '/')
|
|
121
|
+
.replace(/^.*?src\/components\//, '')
|
|
122
|
+
.replace(/[^/]*\.folder\/?/g, '')
|
|
123
|
+
.replace(/\/$/, '')
|
|
124
|
+
inferredRoute = '/components/' + (routeBase ? routeBase + '/' : '') + name
|
|
125
|
+
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/components'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { name, suffix: 'story', ext: storyMatch[2], inferredRoute }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const match = base.match(/^(.+)\.(flow|scene|object|record|prototype|folder)\.(jsonc?)$/)
|
|
132
|
+
if (!match) return null
|
|
133
|
+
|
|
134
|
+
// Skip _-prefixed files (drafts/internal)
|
|
135
|
+
if (match[1].startsWith('_')) return null
|
|
136
|
+
|
|
137
|
+
// Skip files inside _-prefixed directories
|
|
138
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
139
|
+
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
140
|
+
// Normalize .scene → .flow for backward compatibility
|
|
141
|
+
const suffix = match[2] === 'scene' ? 'flow' : match[2]
|
|
142
|
+
let name = match[1]
|
|
143
|
+
|
|
144
|
+
// Detect if this file is inside a .folder/ directory
|
|
145
|
+
const folderDirMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\.folder\//)
|
|
146
|
+
const folderName = folderDirMatch ? folderDirMatch[1] : null
|
|
147
|
+
|
|
148
|
+
// Folder metadata files are keyed by their folder directory name (sans .folder suffix)
|
|
149
|
+
if (suffix === 'folder') {
|
|
150
|
+
if (folderName) {
|
|
151
|
+
name = folderName
|
|
152
|
+
}
|
|
153
|
+
return { name, suffix, ext: match[3] }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Prototype metadata files are keyed by their prototype directory name
|
|
157
|
+
// (skip .folder/ segments when determining prototype name)
|
|
158
|
+
if (suffix === 'prototype') {
|
|
159
|
+
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/(?:[^/]+\.folder\/)?([^/]+)\//)
|
|
160
|
+
if (protoMatch) {
|
|
161
|
+
name = protoMatch[1]
|
|
162
|
+
}
|
|
163
|
+
return { name, suffix, ext: match[3], folder: folderName }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Scope flows, records, and objects inside src/prototypes/{Name}/ with a prefix
|
|
167
|
+
// (skip .folder/ segments when determining prototype name)
|
|
168
|
+
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/(?:[^/]+\.folder\/)?([^/]+)\//)
|
|
169
|
+
if (protoMatch) {
|
|
170
|
+
name = `${protoMatch[1]}/${name}`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Infer route for prototype-scoped flows from their file path.
|
|
174
|
+
// Mirrors the generouted route regex: strip src/prototypes/ and *.folder/ segments.
|
|
175
|
+
let inferredRoute = null
|
|
176
|
+
if (suffix === 'flow') {
|
|
177
|
+
const protoCheck = normalized.match(/(?:^|\/)src\/prototypes\//)
|
|
178
|
+
if (protoCheck) {
|
|
179
|
+
const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
|
|
180
|
+
inferredRoute = '/' + dirPath
|
|
181
|
+
.replace(/^.*?src\/prototypes\//, '')
|
|
182
|
+
.replace(/[^/]*\.folder\//g, '')
|
|
183
|
+
// Normalize trailing slash and double slashes
|
|
184
|
+
inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { name, suffix, ext: match[3], inferredRoute }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Batch-fetch git metadata (author + lastModified) for multiple files in a
|
|
193
|
+
* single subprocess, avoiding per-file git overhead during startup.
|
|
194
|
+
*
|
|
195
|
+
* Returns a Map<absPath, { gitAuthor: string|null, lastModified: string|null }>
|
|
196
|
+
*/
|
|
197
|
+
function batchGitMetadata(root, filePaths) {
|
|
198
|
+
const result = new Map()
|
|
199
|
+
if (filePaths.length === 0) return result
|
|
200
|
+
|
|
201
|
+
// Initialize all entries
|
|
202
|
+
for (const fp of filePaths) {
|
|
203
|
+
result.set(fp, { gitAuthor: null, lastModified: null })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Batch lastModified: one git log call with all paths
|
|
208
|
+
// git log -1 gives the most recent commit touching any of these paths,
|
|
209
|
+
// but we need per-path data. Use --name-only to correlate.
|
|
210
|
+
// For efficiency, use a single git log with --format and --name-only
|
|
211
|
+
// that outputs one record per commit touching these files.
|
|
212
|
+
const allDirs = [...new Set(filePaths.map(fp => path.dirname(fp)))]
|
|
213
|
+
const dirsArg = allDirs.map(d => `"${d}"`).join(' ')
|
|
214
|
+
|
|
215
|
+
// Get lastModified per directory in one call using git log --format
|
|
216
|
+
// We output "MARKER<sep>dir<sep>date" per commit, then take the latest per dir.
|
|
217
|
+
const logResult = execSync(
|
|
218
|
+
`git log --format="%aI" --name-only -- ${dirsArg}`,
|
|
219
|
+
{ cwd: root, encoding: 'utf-8', timeout: 10000, maxBuffer: 1024 * 1024 },
|
|
220
|
+
).trim()
|
|
221
|
+
|
|
222
|
+
if (logResult) {
|
|
223
|
+
// Parse: alternating date lines and filename lines separated by blank lines
|
|
224
|
+
const blocks = logResult.split('\n\n')
|
|
225
|
+
const dirDates = new Map() // dir → most recent date
|
|
226
|
+
for (const block of blocks) {
|
|
227
|
+
const lines = block.split('\n').filter(Boolean)
|
|
228
|
+
if (lines.length < 2) continue
|
|
229
|
+
const date = lines[0]
|
|
230
|
+
for (let li = 1; li < lines.length; li++) {
|
|
231
|
+
const fileLine = lines[li].trim()
|
|
232
|
+
if (!fileLine) continue
|
|
233
|
+
const dir = path.dirname(path.resolve(root, fileLine))
|
|
234
|
+
if (!dirDates.has(dir)) {
|
|
235
|
+
dirDates.set(dir, date)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const fp of filePaths) {
|
|
240
|
+
const dir = path.dirname(fp)
|
|
241
|
+
const entry = result.get(fp)
|
|
242
|
+
if (dirDates.has(dir) && entry) {
|
|
243
|
+
entry.lastModified = dirDates.get(dir)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch { /* git not available or failed — leave nulls */ }
|
|
248
|
+
|
|
249
|
+
// Batch gitAuthor: use git log for each file's creation author.
|
|
250
|
+
// Unfortunately --follow --diff-filter=A doesn't combine well with multiple
|
|
251
|
+
// paths, so batch them in a single shell invocation using a for loop.
|
|
252
|
+
try {
|
|
253
|
+
const relPaths = filePaths.map(fp => path.relative(root, fp))
|
|
254
|
+
// Build a shell script that outputs "PATH<tab>AUTHOR" per file
|
|
255
|
+
const cmds = relPaths.map(rp =>
|
|
256
|
+
`echo -n "${rp}\\t"; git log --follow --diff-filter=A --format="%aN" -- "${rp}" | tail -1`
|
|
257
|
+
).join('; ')
|
|
258
|
+
const authorResult = execSync(cmds, {
|
|
259
|
+
cwd: root, encoding: 'utf-8', timeout: 10000, shell: true, maxBuffer: 1024 * 1024,
|
|
260
|
+
}).trim()
|
|
261
|
+
|
|
262
|
+
if (authorResult) {
|
|
263
|
+
for (const line of authorResult.split('\n')) {
|
|
264
|
+
const tabIdx = line.indexOf('\t')
|
|
265
|
+
if (tabIdx < 0) continue
|
|
266
|
+
const relPath = line.slice(0, tabIdx)
|
|
267
|
+
const author = line.slice(tabIdx + 1).trim()
|
|
268
|
+
if (!author) continue
|
|
269
|
+
const absPath2 = path.resolve(root, relPath)
|
|
270
|
+
const entry = result.get(absPath2)
|
|
271
|
+
if (entry) entry.gitAuthor = author
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch { /* git not available */ }
|
|
275
|
+
|
|
276
|
+
return result
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Scan the repo for all data files, validate uniqueness, return the index.
|
|
281
|
+
*/
|
|
282
|
+
function buildIndex(root) {
|
|
283
|
+
const ignore = ['node_modules/**', 'dist/**', '.git/**', '.worktrees/**', 'public/**']
|
|
284
|
+
const files = globSync(GLOB_PATTERN, { cwd: root, ignore, absolute: false })
|
|
285
|
+
const canvasFiles = globSync(CANVAS_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
|
|
286
|
+
const canvasMetaFiles = globSync(CANVAS_META_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
|
|
287
|
+
const storyFiles = globSync(STORY_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
|
|
288
|
+
|
|
289
|
+
// Detect nested .folder/ directories (not supported)
|
|
290
|
+
// Scan directories directly since empty nested folders have no data files
|
|
291
|
+
const folderDirs = globSync('src/prototypes/**/*.folder', { cwd: root, ignore, absolute: false })
|
|
292
|
+
for (const dir of folderDirs) {
|
|
293
|
+
const normalized = dir.replace(/\\/g, '/')
|
|
294
|
+
const segments = normalized.split('/').filter(s => s.endsWith('.folder'))
|
|
295
|
+
if (segments.length > 1) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`[storyboard-data] Nested .folder directories are not supported.\n` +
|
|
298
|
+
` Found at: ${dir}\n` +
|
|
299
|
+
` Folders can only be one level deep inside src/prototypes/.`
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const index = { flow: {}, object: {}, record: {}, prototype: {}, folder: {}, canvas: {}, 'canvas-meta': {}, story: {} }
|
|
305
|
+
const seen = {} // "name.suffix" or "id.suffix" → absolute path (for duplicate detection)
|
|
306
|
+
const protoFolders = {} // prototype name → folder name (for injection)
|
|
307
|
+
const flowRoutes = {} // flow name → inferred route (for _route injection)
|
|
308
|
+
const canvasRoutes = {} // canvas name → inferred route
|
|
309
|
+
const canvasAliases = {} // basename → canonical ID (only when unique)
|
|
310
|
+
const canvasNameCount = {} // canvas basename → count (for ambiguity detection)
|
|
311
|
+
const canvasGroups = {} // canvas name → group name (shared folder prefix)
|
|
312
|
+
const storyRoutes = {} // story name → inferred route
|
|
313
|
+
|
|
314
|
+
for (const relPath of [...files, ...canvasFiles, ...canvasMetaFiles, ...storyFiles]) {
|
|
315
|
+
const parsed = parseDataFile(relPath)
|
|
316
|
+
if (!parsed) continue
|
|
317
|
+
|
|
318
|
+
// Canvas files use path-based IDs for dedup; others use basename
|
|
319
|
+
const dedupKey = parsed.suffix === 'canvas' && parsed.id
|
|
320
|
+
? `${parsed.id}.${parsed.suffix}`
|
|
321
|
+
: `${parsed.name}.${parsed.suffix}`
|
|
322
|
+
const absPath = path.resolve(root, relPath)
|
|
323
|
+
|
|
324
|
+
if (seen[dedupKey]) {
|
|
325
|
+
const hint = parsed.suffix === 'folder'
|
|
326
|
+
? ' Folder names must be unique across the project.'
|
|
327
|
+
: parsed.suffix === 'canvas'
|
|
328
|
+
? ' Canvas IDs must be unique. Move or rename one file to resolve the collision.'
|
|
329
|
+
: ' Flows, records, and objects are scoped to their prototype directory.\n' +
|
|
330
|
+
' If both files are global (outside src/prototypes/), rename one to avoid the collision.'
|
|
331
|
+
|
|
332
|
+
throw new Error(
|
|
333
|
+
`[storyboard-data] Duplicate ${parsed.suffix} "${parsed.id || parsed.name}"\n` +
|
|
334
|
+
` Found at: ${seen[dedupKey]}\n` +
|
|
335
|
+
` And at: ${absPath}\n` +
|
|
336
|
+
hint
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
seen[dedupKey] = absPath
|
|
341
|
+
|
|
342
|
+
// Canvas: index only by canonical ID. Basename aliases go in a separate map
|
|
343
|
+
// so listCanvases() and viewfinder don't show duplicates.
|
|
344
|
+
if (parsed.suffix === 'canvas' && parsed.id) {
|
|
345
|
+
index.canvas[parsed.id] = absPath
|
|
346
|
+
// Track basename for alias resolution (only when unique)
|
|
347
|
+
canvasNameCount[parsed.name] = (canvasNameCount[parsed.name] || 0) + 1
|
|
348
|
+
if (canvasNameCount[parsed.name] === 1) {
|
|
349
|
+
canvasAliases[parsed.name] = parsed.id
|
|
350
|
+
} else {
|
|
351
|
+
delete canvasAliases[parsed.name]
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
index[parsed.suffix][parsed.name] = absPath
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Track which folder a prototype belongs to
|
|
358
|
+
if (parsed.suffix === 'prototype' && parsed.folder) {
|
|
359
|
+
protoFolders[parsed.name] = parsed.folder
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Track inferred routes for flows
|
|
363
|
+
if (parsed.suffix === 'flow' && parsed.inferredRoute) {
|
|
364
|
+
flowRoutes[parsed.name] = parsed.inferredRoute
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Track inferred routes for canvases (keyed by canonical ID)
|
|
368
|
+
if (parsed.suffix === 'canvas' && parsed.inferredRoute) {
|
|
369
|
+
const canvasKey = parsed.id || parsed.name
|
|
370
|
+
canvasRoutes[canvasKey] = parsed.inferredRoute
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Track canvas groups (canvases sharing a folder prefix)
|
|
374
|
+
// Use canonical ID as key to match the canvas index
|
|
375
|
+
if (parsed.suffix === 'canvas' && parsed.group) {
|
|
376
|
+
const groupKey = parsed.id || parsed.name
|
|
377
|
+
canvasGroups[groupKey] = parsed.group
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Track inferred routes for stories
|
|
381
|
+
if (parsed.suffix === 'story' && parsed.inferredRoute) {
|
|
382
|
+
storyRoutes[parsed.name] = parsed.inferredRoute
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return { index, protoFolders, flowRoutes, canvasRoutes, canvasAliases, canvasGroups, storyRoutes }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Recursively walk a parsed JSON value and replace `${varName}` patterns
|
|
391
|
+
* in every string value. Only string values are processed — keys, numbers,
|
|
392
|
+
* booleans, and null are left untouched.
|
|
393
|
+
*/
|
|
394
|
+
function resolveTemplateVars(obj, vars) {
|
|
395
|
+
if (typeof obj === 'string') {
|
|
396
|
+
let result = obj
|
|
397
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
398
|
+
result = result.replaceAll(`\${${key}}`, value)
|
|
399
|
+
}
|
|
400
|
+
return result
|
|
401
|
+
}
|
|
402
|
+
if (Array.isArray(obj)) return obj.map(item => resolveTemplateVars(item, vars))
|
|
403
|
+
if (obj !== null && typeof obj === 'object') {
|
|
404
|
+
const out = {}
|
|
405
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
406
|
+
out[key] = resolveTemplateVars(value, vars)
|
|
407
|
+
}
|
|
408
|
+
return out
|
|
409
|
+
}
|
|
410
|
+
return obj
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Compute path-based template variables for a data file.
|
|
415
|
+
*
|
|
416
|
+
* - currentDir: directory of the file, relative to project root
|
|
417
|
+
* - currentProto: path to the prototype directory (e.g. src/prototypes/main.folder/Example)
|
|
418
|
+
* - currentProtoDir: path to the first parent *.folder directory (e.g. src/prototypes/main.folder)
|
|
419
|
+
*/
|
|
420
|
+
function computeTemplateVars(absPath, root) {
|
|
421
|
+
const relPath = path.relative(root, absPath).replace(/\\/g, '/')
|
|
422
|
+
const currentDir = path.dirname(relPath).replace(/\\/g, '/')
|
|
423
|
+
|
|
424
|
+
const protoMatch = relPath.match(/^(src\/prototypes\/(?:[^/]+\.folder\/)?[^/]+)\//)
|
|
425
|
+
const currentProto = protoMatch && !protoMatch[1].endsWith('.folder') ? protoMatch[1] : ''
|
|
426
|
+
|
|
427
|
+
const folderMatch = relPath.match(/^(src\/prototypes\/[^/]+\.folder)\//)
|
|
428
|
+
const currentProtoDir = folderMatch ? folderMatch[1] : ''
|
|
429
|
+
|
|
430
|
+
return { currentDir, currentProto, currentProtoDir }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate the virtual module source code.
|
|
435
|
+
* Reads each data file, parses JSONC at build time, and emits pre-parsed
|
|
436
|
+
* JavaScript objects — no runtime parsing needed.
|
|
437
|
+
*/
|
|
438
|
+
/**
|
|
439
|
+
* Read storyboard.config.json from the project root (if it exists).
|
|
440
|
+
* Returns the parsed and defaulted config object, or null if not found.
|
|
441
|
+
*/
|
|
442
|
+
function readConfig(root) {
|
|
443
|
+
const configPath = path.resolve(root, 'storyboard.config.json')
|
|
444
|
+
try {
|
|
445
|
+
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
446
|
+
const errors = []
|
|
447
|
+
const config = parseJsonc(raw, errors)
|
|
448
|
+
// Treat malformed JSON (e.g. mid-edit partial saves) as missing config
|
|
449
|
+
if (errors.length > 0) return { config: null, configPath }
|
|
450
|
+
return { config: getConfig(config), configPath }
|
|
451
|
+
} catch {
|
|
452
|
+
return { config: null, configPath }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Read toolbar.config.json from @dfosco/storyboard.
|
|
458
|
+
* Returns the full config object with modes array.
|
|
459
|
+
* Falls back to hardcoded defaults if not found.
|
|
460
|
+
*/
|
|
461
|
+
function readModesConfig(root) {
|
|
462
|
+
const fallback = {
|
|
463
|
+
modes: [
|
|
464
|
+
{ name: 'prototype', label: 'Navigate', hue: '#2a2a2a' },
|
|
465
|
+
{ name: 'inspect', label: 'Develop', hue: '#7655a4' },
|
|
466
|
+
{ name: 'present', label: 'Collaborate', hue: '#2a9d8f' },
|
|
467
|
+
{ name: 'plan', label: 'Canvas', hue: '#4a7fad' },
|
|
468
|
+
],
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Try local workspace path first (monorepo), then node_modules
|
|
472
|
+
const candidates = [
|
|
473
|
+
path.resolve(root, 'packages/storyboard/toolbar.config.json'),
|
|
474
|
+
path.resolve(root, 'packages/storyboard/configs/modes.config.json'),
|
|
475
|
+
path.resolve(root, 'node_modules/@dfosco/storyboard/toolbar.config.json'),
|
|
476
|
+
path.resolve(root, 'node_modules/@dfosco/storyboard/configs/modes.config.json'),
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
for (const filePath of candidates) {
|
|
480
|
+
try {
|
|
481
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
482
|
+
const parsed = JSON.parse(raw)
|
|
483
|
+
if (Array.isArray(parsed.modes) && parsed.modes.length > 0) {
|
|
484
|
+
return { modes: parsed.modes }
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
// try next candidate
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return fallback
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Read a JSON/JSONC file, returning null on failure.
|
|
496
|
+
*/
|
|
497
|
+
function readJsonFile(filePath) {
|
|
498
|
+
try {
|
|
499
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
500
|
+
const errors = []
|
|
501
|
+
const parsed = parseJsonc(raw, errors)
|
|
502
|
+
return errors.length === 0 ? parsed : null
|
|
503
|
+
} catch {
|
|
504
|
+
return null
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Find a core config file from either the monorepo workspace or node_modules.
|
|
510
|
+
*/
|
|
511
|
+
function readCoreConfigFile(root, filename) {
|
|
512
|
+
const candidates = [
|
|
513
|
+
path.resolve(root, `packages/storyboard/${filename}`),
|
|
514
|
+
path.resolve(root, `node_modules/@dfosco/storyboard/${filename}`),
|
|
515
|
+
]
|
|
516
|
+
for (const p of candidates) {
|
|
517
|
+
const parsed = readJsonFile(p)
|
|
518
|
+
if (parsed) return parsed
|
|
519
|
+
}
|
|
520
|
+
return null
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Deep-merge helper (same as loader.js deepMerge but available at build time).
|
|
525
|
+
* Arrays are replaced, not concatenated. Objects are recursively merged.
|
|
526
|
+
*/
|
|
527
|
+
function deepMergeBuild(target, source) {
|
|
528
|
+
if (!source || typeof source !== 'object') return target
|
|
529
|
+
if (!target || typeof target !== 'object') return source
|
|
530
|
+
const result = { ...target }
|
|
531
|
+
for (const key of Object.keys(source)) {
|
|
532
|
+
const sv = source[key]
|
|
533
|
+
const tv = target[key]
|
|
534
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
535
|
+
result[key] = deepMergeBuild(tv, sv)
|
|
536
|
+
} else if (Array.isArray(sv) && Array.isArray(tv) && sv.length > 0 && tv.length > 0 && sv[0]?.id && tv[0]?.id) {
|
|
537
|
+
// Id-based array merge: override matching entries by id, keep the rest, append new ones
|
|
538
|
+
const targetMap = new Map(tv.map(item => [item.id, item]))
|
|
539
|
+
for (const item of sv) {
|
|
540
|
+
targetMap.set(item.id, item.id && targetMap.has(item.id)
|
|
541
|
+
? deepMergeBuild(targetMap.get(item.id), item)
|
|
542
|
+
: item)
|
|
543
|
+
}
|
|
544
|
+
result[key] = [...targetMap.values()]
|
|
545
|
+
} else {
|
|
546
|
+
result[key] = sv
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return result
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Build the unified config object by reading and merging all config sources.
|
|
554
|
+
*
|
|
555
|
+
* Priority (lowest → highest):
|
|
556
|
+
* configSchema defaults → core domain configs → storyboard.config.json → user domain configs
|
|
557
|
+
*
|
|
558
|
+
* Domain-specific config files (toolbar.config.json, commandpalette.config.json, etc.)
|
|
559
|
+
* always win over storyboard.config.json — specificity beats generality.
|
|
560
|
+
* Deep merge is used at every layer: objects are recursively merged (keys append),
|
|
561
|
+
* arrays and scalars are replaced.
|
|
562
|
+
*
|
|
563
|
+
* Returns { unified, warnings } where warnings is an array of overlap messages.
|
|
564
|
+
*/
|
|
565
|
+
function buildUnifiedConfig(root) {
|
|
566
|
+
const warnings = []
|
|
567
|
+
|
|
568
|
+
// 1. Read core defaults (lowest priority domain configs)
|
|
569
|
+
const coreToolbar = readCoreConfigFile(root, 'toolbar.config.json') || {}
|
|
570
|
+
const coreCommandPalette = readCoreConfigFile(root, 'commandpalette.config.json') || {}
|
|
571
|
+
const corePaste = readCoreConfigFile(root, 'paste.config.json') || {}
|
|
572
|
+
const coreWidgets = readCoreConfigFile(root, 'widgets.config.json') || {}
|
|
573
|
+
|
|
574
|
+
// 2. Read storyboard.config.json (middle priority)
|
|
575
|
+
// Use the schema-defaulted config for most things, but also read
|
|
576
|
+
// the raw file to know which keys were explicitly set by the user.
|
|
577
|
+
const { config: sbConfig } = readConfig(root)
|
|
578
|
+
const rawSbConfig = readJsonFile(path.resolve(root, 'storyboard.config.json')) || {}
|
|
579
|
+
|
|
580
|
+
// 3. Apply storyboard.config.json overrides on top of core domain configs.
|
|
581
|
+
// Only merge when the user explicitly defined the key in storyboard.config.json
|
|
582
|
+
// (not from configSchema defaults, which would overwrite core config with empty arrays).
|
|
583
|
+
const afterSbToolbar = rawSbConfig.toolbar
|
|
584
|
+
? deepMergeBuild(coreToolbar, sbConfig.toolbar)
|
|
585
|
+
: coreToolbar
|
|
586
|
+
const afterSbCommandPalette = rawSbConfig.commandPalette
|
|
587
|
+
? deepMergeBuild(coreCommandPalette, sbConfig.commandPalette)
|
|
588
|
+
: coreCommandPalette
|
|
589
|
+
const afterSbPaste = rawSbConfig.paste
|
|
590
|
+
? deepMergeBuild(corePaste, sbConfig.paste || {})
|
|
591
|
+
: corePaste
|
|
592
|
+
const afterSbWidgets = rawSbConfig.widgets
|
|
593
|
+
? deepMergeBuild(coreWidgets, sbConfig.widgets || {})
|
|
594
|
+
: coreWidgets
|
|
595
|
+
|
|
596
|
+
// 4. Read user domain config files (highest priority)
|
|
597
|
+
const userFiles = [
|
|
598
|
+
{ domain: 'widgets', filename: 'widgets.config.json' },
|
|
599
|
+
{ domain: 'paste', filename: 'paste.config.json' },
|
|
600
|
+
{ domain: 'toolbar', filename: 'toolbar.config.json' },
|
|
601
|
+
{ domain: 'commandPalette', filename: 'commandpalette.config.json' },
|
|
602
|
+
]
|
|
603
|
+
|
|
604
|
+
const userConfigs = {}
|
|
605
|
+
for (const { domain, filename } of userFiles) {
|
|
606
|
+
const filePath = path.resolve(root, filename)
|
|
607
|
+
const parsed = readJsonFile(filePath)
|
|
608
|
+
if (parsed) userConfigs[domain] = { data: parsed, filename }
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 5. Apply user domain configs on top of everything (highest priority)
|
|
612
|
+
const finalToolbar = userConfigs.toolbar
|
|
613
|
+
? deepMergeBuild(afterSbToolbar, userConfigs.toolbar.data)
|
|
614
|
+
: afterSbToolbar
|
|
615
|
+
const finalCommandPalette = userConfigs.commandPalette
|
|
616
|
+
? deepMergeBuild(afterSbCommandPalette, userConfigs.commandPalette.data)
|
|
617
|
+
: afterSbCommandPalette
|
|
618
|
+
const finalPaste = userConfigs.paste
|
|
619
|
+
? deepMergeBuild(afterSbPaste, userConfigs.paste.data)
|
|
620
|
+
: afterSbPaste
|
|
621
|
+
const finalWidgets = userConfigs.widgets
|
|
622
|
+
? deepMergeBuild(afterSbWidgets, userConfigs.widgets.data)
|
|
623
|
+
: afterSbWidgets
|
|
624
|
+
|
|
625
|
+
// 6. Detect overlaps between storyboard.config.json and user domain configs
|
|
626
|
+
const domainOverlapChecks = [
|
|
627
|
+
{ sbKey: 'toolbar', domain: 'toolbar', label: 'toolbar.config.json' },
|
|
628
|
+
{ sbKey: 'commandPalette', domain: 'commandPalette', label: 'commandpalette.config.json' },
|
|
629
|
+
{ sbKey: 'paste', domain: 'paste', label: 'paste.config.json' },
|
|
630
|
+
{ sbKey: 'widgets', domain: 'widgets', label: 'widgets.config.json' },
|
|
631
|
+
]
|
|
632
|
+
for (const { sbKey, domain, label } of domainOverlapChecks) {
|
|
633
|
+
if (rawSbConfig[sbKey] && userConfigs[domain]) {
|
|
634
|
+
const overlaps = findOverlappingKeys(rawSbConfig[sbKey], userConfigs[domain].data)
|
|
635
|
+
for (const key of overlaps) {
|
|
636
|
+
warnings.push(`Config overlap: "${key}" is defined in both storyboard.config.json.${sbKey} and ${label} — ${label} wins.`)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 7. Build the unified config object
|
|
642
|
+
const unified = {
|
|
643
|
+
toolbar: finalToolbar,
|
|
644
|
+
commandPalette: finalCommandPalette,
|
|
645
|
+
paste: finalPaste,
|
|
646
|
+
widgets: finalWidgets,
|
|
647
|
+
featureFlags: sbConfig?.featureFlags || {},
|
|
648
|
+
modes: sbConfig?.modes || {},
|
|
649
|
+
ui: sbConfig?.ui || {},
|
|
650
|
+
canvas: sbConfig?.canvas || {},
|
|
651
|
+
comments: sbConfig?.comments || {},
|
|
652
|
+
customerMode: sbConfig?.customerMode || {},
|
|
653
|
+
plugins: sbConfig?.plugins || {},
|
|
654
|
+
repository: sbConfig?.repository || {},
|
|
655
|
+
workshop: sbConfig?.workshop || {},
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return { unified, warnings }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Find top-level keys that exist in both objects (overlap detection).
|
|
663
|
+
*/
|
|
664
|
+
function findOverlappingKeys(a, b, prefix = '') {
|
|
665
|
+
const overlaps = []
|
|
666
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return overlaps
|
|
667
|
+
for (const key of Object.keys(a)) {
|
|
668
|
+
if (key in b) {
|
|
669
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
670
|
+
overlaps.push(path)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return overlaps
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasAliases, canvasGroups, storyRoutes }, root) {
|
|
677
|
+
const declarations = []
|
|
678
|
+
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder', 'canvas']
|
|
679
|
+
const entries = { flow: [], object: [], record: [], prototype: [], folder: [], canvas: [] }
|
|
680
|
+
const storyEntries = [] // handled separately (code modules, not JSON data)
|
|
681
|
+
const resolvedFlowRoutes = {} // flow name → resolved route (for multi-flow logging)
|
|
682
|
+
let i = 0
|
|
683
|
+
|
|
684
|
+
// Batch-fetch git metadata for all prototype + canvas files in 1-2 subprocesses
|
|
685
|
+
const gitPaths = [
|
|
686
|
+
...Object.values(index.prototype || {}),
|
|
687
|
+
...Object.values(index.canvas || {}),
|
|
688
|
+
]
|
|
689
|
+
const gitMeta = batchGitMetadata(root, gitPaths)
|
|
690
|
+
|
|
691
|
+
// Read canvas-meta files and build a directory-based lookup
|
|
692
|
+
const canvasMetaByDir = {}
|
|
693
|
+
for (const [, absPath] of Object.entries(index['canvas-meta'] || {})) {
|
|
694
|
+
try {
|
|
695
|
+
const raw = fs.readFileSync(absPath, 'utf-8')
|
|
696
|
+
const parsed = parseJsonc(raw)
|
|
697
|
+
if (parsed) {
|
|
698
|
+
// Key by the parent directory path relative to src/canvas/
|
|
699
|
+
const dirPath = path.dirname(absPath).replace(/\\/g, '/')
|
|
700
|
+
const canvasRelDir = dirPath.replace(/^.*?src\/canvas\//, '')
|
|
701
|
+
canvasMetaByDir[canvasRelDir] = parsed
|
|
702
|
+
}
|
|
703
|
+
} catch { /* skip invalid meta files */ }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
for (const suffix of INDEX_KEYS) {
|
|
707
|
+
for (const [name, absPath] of Object.entries(index[suffix])) {
|
|
708
|
+
const varName = `_d${i++}`
|
|
709
|
+
const raw = fs.readFileSync(absPath, 'utf-8')
|
|
710
|
+
let parsed = suffix === 'canvas'
|
|
711
|
+
? materializeFromText(raw)
|
|
712
|
+
: parseJsonc(raw)
|
|
713
|
+
|
|
714
|
+
// Auto-fill gitAuthor for prototype metadata from git history
|
|
715
|
+
if (suffix === 'prototype' && parsed && !parsed.gitAuthor) {
|
|
716
|
+
const meta = gitMeta.get(absPath)
|
|
717
|
+
if (meta?.gitAuthor) {
|
|
718
|
+
parsed = { ...parsed, gitAuthor: meta.gitAuthor }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Auto-fill lastModified from git history for prototypes
|
|
723
|
+
if (suffix === 'prototype' && parsed) {
|
|
724
|
+
const meta = gitMeta.get(absPath)
|
|
725
|
+
if (meta?.lastModified) {
|
|
726
|
+
parsed = { ...parsed, lastModified: meta.lastModified }
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Inject folder association into prototype metadata
|
|
731
|
+
if (suffix === 'prototype' && protoFolders[name]) {
|
|
732
|
+
parsed = { ...parsed, folder: protoFolders[name] }
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Load prototype-level config overrides from the prototype directory.
|
|
736
|
+
// Any config file placed alongside the .prototype.json becomes an override
|
|
737
|
+
// for that domain when the prototype is active.
|
|
738
|
+
if (suffix === 'prototype') {
|
|
739
|
+
const protoDir = path.dirname(absPath)
|
|
740
|
+
const protoConfigFiles = [
|
|
741
|
+
{ filename: 'toolbar.config.json', key: 'toolbarConfig' },
|
|
742
|
+
{ filename: 'commandpalette.config.json', key: 'commandPaletteConfig' },
|
|
743
|
+
{ filename: 'widgets.config.json', key: 'widgetsConfig' },
|
|
744
|
+
{ filename: 'paste.config.json', key: 'pasteConfig' },
|
|
745
|
+
]
|
|
746
|
+
for (const { filename, key } of protoConfigFiles) {
|
|
747
|
+
const cfgPath = path.join(protoDir, filename)
|
|
748
|
+
if (fs.existsSync(cfgPath)) {
|
|
749
|
+
try {
|
|
750
|
+
const raw = fs.readFileSync(cfgPath, 'utf-8')
|
|
751
|
+
const cfg = parseJsonc(raw)
|
|
752
|
+
if (cfg) {
|
|
753
|
+
parsed = { ...parsed, [key]: cfg }
|
|
754
|
+
}
|
|
755
|
+
} catch { /* skip invalid config */ }
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Inject inferred _route into flow data (explicit route takes precedence)
|
|
761
|
+
if (suffix === 'flow' && flowRoutes[name] && !parsed?.route) {
|
|
762
|
+
parsed = { ...parsed, _route: flowRoutes[name] }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Track resolved route for multi-flow logging
|
|
766
|
+
if (suffix === 'flow') {
|
|
767
|
+
const route = parsed?.route || parsed?._route || null
|
|
768
|
+
if (route) {
|
|
769
|
+
resolvedFlowRoutes[name] = { route, isDefault: parsed?.meta?.default === true }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Auto-fill gitAuthor for canvas metadata from git history
|
|
774
|
+
if (suffix === 'canvas' && parsed && !parsed.gitAuthor) {
|
|
775
|
+
const meta = gitMeta.get(absPath)
|
|
776
|
+
if (meta?.gitAuthor) {
|
|
777
|
+
parsed = { ...parsed, gitAuthor: meta.gitAuthor }
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Inject inferred route, group, and resolve JSX companion for canvases
|
|
782
|
+
if (suffix === 'canvas') {
|
|
783
|
+
if (canvasRoutes[name]) {
|
|
784
|
+
parsed = { ...parsed, _route: canvasRoutes[name] }
|
|
785
|
+
}
|
|
786
|
+
if (canvasGroups[name]) {
|
|
787
|
+
parsed = { ...parsed, _group: canvasGroups[name] }
|
|
788
|
+
}
|
|
789
|
+
// Inject canvas folder metadata from .meta.json
|
|
790
|
+
if (canvasGroups[name] && canvasMetaByDir[canvasGroups[name]]) {
|
|
791
|
+
parsed = { ...parsed, _canvasMeta: canvasMetaByDir[canvasGroups[name]] }
|
|
792
|
+
}
|
|
793
|
+
// Inject folder association
|
|
794
|
+
const folderDirMatch = path.relative(root, absPath).replace(/\\/g, '/').match(/(?:^|\/)src\/(?:prototypes|canvas)\/([^/]+)\.folder\//)
|
|
795
|
+
if (folderDirMatch) {
|
|
796
|
+
parsed = { ...parsed, _folder: folderDirMatch[1] }
|
|
797
|
+
}
|
|
798
|
+
// Resolve JSX companion file path
|
|
799
|
+
if (parsed?.jsx) {
|
|
800
|
+
const jsxPath = path.resolve(path.dirname(absPath), parsed.jsx)
|
|
801
|
+
if (fs.existsSync(jsxPath)) {
|
|
802
|
+
const relJsx = '/' + path.relative(root, jsxPath).replace(/\\/g, '/')
|
|
803
|
+
parsed = { ...parsed, _jsxModule: relJsx }
|
|
804
|
+
} else {
|
|
805
|
+
console.warn(
|
|
806
|
+
`[storyboard-data] Canvas "${name}" references JSX file "${parsed.jsx}" but it was not found at ${jsxPath}`
|
|
807
|
+
)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Resolve template variables (${currentDir}, ${currentProto}, ${currentProtoDir})
|
|
813
|
+
const templateVars = computeTemplateVars(absPath, root)
|
|
814
|
+
if (!templateVars.currentProto && raw.includes('${currentProto}')) {
|
|
815
|
+
console.warn(
|
|
816
|
+
`[storyboard-data] \${currentProto} used in "${path.relative(root, absPath)}" ` +
|
|
817
|
+
`but file is not inside a prototype directory. Variable resolves to empty string.`
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
if (!templateVars.currentProtoDir && raw.includes('${currentProtoDir}')) {
|
|
821
|
+
console.warn(
|
|
822
|
+
`[storyboard-data] \${currentProtoDir} used in "${path.relative(root, absPath)}" ` +
|
|
823
|
+
`but file is not inside a .folder directory. Variable resolves to empty string.`
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
parsed = resolveTemplateVars(parsed, templateVars)
|
|
827
|
+
|
|
828
|
+
if (suffix === 'canvas' && parsed._jsxModule) {
|
|
829
|
+
declarations.push(`const ${varName} = Object.assign(${JSON.stringify(parsed)}, { _jsxImport: () => import(${JSON.stringify(parsed._jsxModule)}) })`)
|
|
830
|
+
} else {
|
|
831
|
+
declarations.push(`const ${varName} = ${JSON.stringify(parsed)}`)
|
|
832
|
+
}
|
|
833
|
+
entries[suffix].push(` ${JSON.stringify(name)}: ${varName}`)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Generate story entries (code modules with dynamic imports, not JSON data)
|
|
838
|
+
for (const [name, absPath] of Object.entries(index.story || {})) {
|
|
839
|
+
const varName = `_d${i++}`
|
|
840
|
+
const relModule = '/' + path.relative(root, absPath).replace(/\\/g, '/')
|
|
841
|
+
const storyMeta = { _storyModule: relModule }
|
|
842
|
+
if (storyRoutes[name]) {
|
|
843
|
+
storyMeta._route = storyRoutes[name]
|
|
844
|
+
}
|
|
845
|
+
declarations.push(
|
|
846
|
+
`const ${varName} = Object.assign(${JSON.stringify(storyMeta)}, { _storyImport: () => import(${JSON.stringify(relModule)}) })`
|
|
847
|
+
)
|
|
848
|
+
storyEntries.push(` ${JSON.stringify(name)}: ${varName}`)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const imports = [`import { init } from '@dfosco/storyboard/core'`]
|
|
852
|
+
const initCalls = [`init({ flows, objects, records, prototypes, folders, canvases, stories })`]
|
|
853
|
+
|
|
854
|
+
// Build unified config from all sources
|
|
855
|
+
const { unified: unifiedConfig, warnings: configWarnings } = buildUnifiedConfig(root)
|
|
856
|
+
for (const w of configWarnings) {
|
|
857
|
+
console.warn(`[storyboard] ⚠ ${w}`)
|
|
858
|
+
}
|
|
859
|
+
imports.push(`import { initConfig } from '@dfosco/storyboard/core'`)
|
|
860
|
+
initCalls.push(`initConfig(${JSON.stringify(unifiedConfig)})`)
|
|
861
|
+
|
|
862
|
+
// Feature flags from storyboard.config.json
|
|
863
|
+
const { config } = readConfig(root)
|
|
864
|
+
if (config?.featureFlags && Object.keys(config.featureFlags).length > 0) {
|
|
865
|
+
imports.push(`import { initFeatureFlags } from '@dfosco/storyboard/core'`)
|
|
866
|
+
initCalls.push(`initFeatureFlags(${JSON.stringify(config.featureFlags)})`)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Plugin configuration from storyboard.config.json
|
|
870
|
+
if (config?.plugins && Object.keys(config.plugins).length > 0) {
|
|
871
|
+
imports.push(`import { initPlugins } from '@dfosco/storyboard/core'`)
|
|
872
|
+
initCalls.push(`initPlugins(${JSON.stringify(config.plugins)})`)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Modes configuration from storyboard.config.json
|
|
876
|
+
if (config?.modes) {
|
|
877
|
+
imports.push(`import { initModesConfig, registerMode, syncModeClasses, initTools } from '@dfosco/storyboard/core'`)
|
|
878
|
+
initCalls.push(`initModesConfig(${JSON.stringify(config.modes)})`)
|
|
879
|
+
|
|
880
|
+
if (config.modes.enabled) {
|
|
881
|
+
imports.push(`import '@dfosco/storyboard/modes.css'`)
|
|
882
|
+
|
|
883
|
+
const modesConfig = readModesConfig(root)
|
|
884
|
+
const modes = config.modes.defaults || modesConfig.modes
|
|
885
|
+
for (const m of modes) {
|
|
886
|
+
initCalls.push(`registerMode(${JSON.stringify(m.name)}, { label: ${JSON.stringify(m.label)} })`)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
initCalls.push(`syncModeClasses()`)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// UI config from storyboard.config.json (menu visibility overrides)
|
|
894
|
+
if (config?.ui) {
|
|
895
|
+
imports.push(`import { initUIConfig } from '@dfosco/storyboard/core'`)
|
|
896
|
+
initCalls.push(`initUIConfig(${JSON.stringify(config.ui)})`)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Customer mode config from storyboard.config.json
|
|
900
|
+
if (config?.customerMode) {
|
|
901
|
+
imports.push(`import { initCustomerModeConfig } from '@dfosco/storyboard/core'`)
|
|
902
|
+
initCalls.push(`initCustomerModeConfig(${JSON.stringify(config.customerMode)})`)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Client toolbar overrides from root toolbar.config.json
|
|
906
|
+
const clientToolbarPath = path.resolve(root, 'toolbar.config.json')
|
|
907
|
+
try {
|
|
908
|
+
if (fs.existsSync(clientToolbarPath)) {
|
|
909
|
+
const raw = fs.readFileSync(clientToolbarPath, 'utf-8')
|
|
910
|
+
const errors = []
|
|
911
|
+
const parsed = parseJsonc(raw, errors)
|
|
912
|
+
if (parsed && errors.length === 0) {
|
|
913
|
+
imports.push(`import { setClientToolbarOverrides } from '@dfosco/storyboard/core'`)
|
|
914
|
+
initCalls.push(`setClientToolbarOverrides(${JSON.stringify(parsed)})`)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} catch { /* skip if unreadable */ }
|
|
918
|
+
|
|
919
|
+
// Log info when multiple flows target the same route
|
|
920
|
+
const routeGroups = {}
|
|
921
|
+
for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
|
|
922
|
+
if (!routeGroups[route]) routeGroups[route] = []
|
|
923
|
+
routeGroups[route].push({ name, isDefault })
|
|
924
|
+
}
|
|
925
|
+
for (const [route, flows] of Object.entries(routeGroups)) {
|
|
926
|
+
if (flows.length > 1) {
|
|
927
|
+
const defaults = flows.filter(f => f.isDefault)
|
|
928
|
+
if (defaults.length > 1) {
|
|
929
|
+
console.warn(
|
|
930
|
+
`[storyboard-data] Warning: Route "${route}" has ${defaults.length} flows with meta.default: true.\n` +
|
|
931
|
+
` Only one flow per route should be marked as default.`
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return [
|
|
938
|
+
imports.join('\n'),
|
|
939
|
+
'',
|
|
940
|
+
declarations.join('\n'),
|
|
941
|
+
'',
|
|
942
|
+
`const flows = {\n${entries.flow.join(',\n')}\n}`,
|
|
943
|
+
`const objects = {\n${entries.object.join(',\n')}\n}`,
|
|
944
|
+
`const records = {\n${entries.record.join(',\n')}\n}`,
|
|
945
|
+
`const prototypes = {\n${entries.prototype.join(',\n')}\n}`,
|
|
946
|
+
`const folders = {\n${entries.folder.join(',\n')}\n}`,
|
|
947
|
+
`const canvases = {\n${entries.canvas.join(',\n')}\n}`,
|
|
948
|
+
`const stories = {\n${storyEntries.join(',\n')}\n}`,
|
|
949
|
+
'',
|
|
950
|
+
`// Legacy basename → canonical ID aliases (only unique basenames)`,
|
|
951
|
+
`const canvasAliases = ${JSON.stringify(canvasAliases || {})}`,
|
|
952
|
+
'',
|
|
953
|
+
'// Backward-compatible alias',
|
|
954
|
+
'const scenes = flows',
|
|
955
|
+
'',
|
|
956
|
+
initCalls.join('\n'),
|
|
957
|
+
'',
|
|
958
|
+
`export { flows, scenes, objects, records, prototypes, folders, canvases, canvasAliases, stories }`,
|
|
959
|
+
`export const index = { flows, scenes, objects, records, prototypes, folders, canvases, canvasAliases, stories }`,
|
|
960
|
+
`export default index`,
|
|
961
|
+
'',
|
|
962
|
+
'// Live-patch canvas data on HMR events so SPA navigation shows fresh state',
|
|
963
|
+
'if (import.meta.hot) {',
|
|
964
|
+
' import.meta.hot.on("storyboard:canvas-file-changed", (data) => {',
|
|
965
|
+
' if (!data) return',
|
|
966
|
+
' const id = data.canvasId || data.name',
|
|
967
|
+
' if (data.removed) {',
|
|
968
|
+
' delete canvases[id]',
|
|
969
|
+
' } else if (data.metadata) {',
|
|
970
|
+
' // Merge into existing entry to preserve build-time fields (_jsxModule, _jsxImport, etc.)',
|
|
971
|
+
' canvases[id] = canvases[id]',
|
|
972
|
+
' ? Object.assign({}, canvases[id], data.metadata)',
|
|
973
|
+
' : data.metadata',
|
|
974
|
+
' }',
|
|
975
|
+
' init({ flows, objects, records, prototypes, folders, canvases, stories })',
|
|
976
|
+
' })',
|
|
977
|
+
' import.meta.hot.on("storyboard:story-file-changed", (data) => {',
|
|
978
|
+
' if (!data) return',
|
|
979
|
+
' if (data.removed) {',
|
|
980
|
+
' delete stories[data.name]',
|
|
981
|
+
' } else {',
|
|
982
|
+
' stories[data.name] = { _storyModule: data._storyModule, _route: data._route,',
|
|
983
|
+
' _storyImport: () => import(/* @vite-ignore */ data._storyModule) }',
|
|
984
|
+
' }',
|
|
985
|
+
' init({ flows, objects, records, prototypes, folders, canvases, stories })',
|
|
986
|
+
' document.dispatchEvent(new CustomEvent("storyboard:story-index-changed"))',
|
|
987
|
+
' })',
|
|
988
|
+
'}',
|
|
989
|
+
].join('\n')
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Vite plugin for storyboard data discovery.
|
|
994
|
+
*
|
|
995
|
+
* - Scans the repo for *.flow.json, *.scene.json (compat), *.object.json, *.record.json, *.canvas.jsonl, *.story.{jsx,tsx}
|
|
996
|
+
* - Validates no two files share the same name+suffix (hard build error)
|
|
997
|
+
* - Generates a virtual module `virtual:storyboard-data-index`
|
|
998
|
+
* - Watches for file additions/removals in dev mode
|
|
999
|
+
*/
|
|
1000
|
+
export default function storyboardDataPlugin() {
|
|
1001
|
+
let root = ''
|
|
1002
|
+
let buildResult = null
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
name: 'storyboard-data',
|
|
1006
|
+
enforce: 'pre',
|
|
1007
|
+
|
|
1008
|
+
config() {
|
|
1009
|
+
return {
|
|
1010
|
+
optimizeDeps: {
|
|
1011
|
+
// @dfosco/storyboard is excluded (virtual module), so Vite
|
|
1012
|
+
// can't trace into its deps. Include the remark entry points so
|
|
1013
|
+
// Vite pre-bundles the full chain — covers all transitive CJS
|
|
1014
|
+
// packages (debug, extend, etc.) without whack-a-mole.
|
|
1015
|
+
include: ['cmdk', 'remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
1016
|
+
exclude: ['@dfosco/storyboard'],
|
|
1017
|
+
},
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
configResolved(config) {
|
|
1022
|
+
root = config.root
|
|
1023
|
+
},
|
|
1024
|
+
|
|
1025
|
+
resolveId(id) {
|
|
1026
|
+
if (id === VIRTUAL_MODULE_ID) return RESOLVED_ID
|
|
1027
|
+
},
|
|
1028
|
+
|
|
1029
|
+
load(id) {
|
|
1030
|
+
if (id !== RESOLVED_ID) return null
|
|
1031
|
+
if (!buildResult) buildResult = buildIndex(root)
|
|
1032
|
+
return generateModule(buildResult, root)
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
configureServer(server) {
|
|
1036
|
+
// ── Component isolate middleware ───────────────────────────────
|
|
1037
|
+
// Serves a minimal HTML shell for iframe-isolated component widgets.
|
|
1038
|
+
// The iframe loads componentIsolate.jsx which reads query params
|
|
1039
|
+
// (module, export, theme) and renders a single story export.
|
|
1040
|
+
const isolateEntryPath = new URL('../canvas/componentIsolate.jsx', import.meta.url).pathname
|
|
1041
|
+
// Component-set isolate — renders all exports in a grid, bypassing the full SPA.
|
|
1042
|
+
const componentSetIsolateEntryPath = new URL('../canvas/componentSetIsolate.jsx', import.meta.url).pathname
|
|
1043
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1044
|
+
if (!req.url) return next()
|
|
1045
|
+
let url = req.url
|
|
1046
|
+
const baseNoTrail = (server.config.base || '/').replace(/\/$/, '')
|
|
1047
|
+
if (baseNoTrail && url.startsWith(baseNoTrail)) {
|
|
1048
|
+
url = url.slice(baseNoTrail.length) || '/'
|
|
1049
|
+
}
|
|
1050
|
+
// Match both single-component and component-set isolate routes
|
|
1051
|
+
const isComponentSet = url.startsWith('/_storyboard/canvas/isolate-set')
|
|
1052
|
+
const isSingle = !isComponentSet && url.startsWith('/_storyboard/canvas/isolate')
|
|
1053
|
+
if (!isSingle && !isComponentSet) return next()
|
|
1054
|
+
|
|
1055
|
+
const entryPath = isComponentSet ? componentSetIsolateEntryPath : isolateEntryPath
|
|
1056
|
+
const rawHtml = [
|
|
1057
|
+
'<!DOCTYPE html>',
|
|
1058
|
+
'<html><head>',
|
|
1059
|
+
'<style>html,body{margin:0;padding:0;width:100%;height:100%;background:var(--bgColor-default,transparent)}#root{width:100%;height:100%}</style>',
|
|
1060
|
+
'</head><body>',
|
|
1061
|
+
'<div id="root"></div>',
|
|
1062
|
+
`<script type="module" src="/@fs${entryPath}"></script>`,
|
|
1063
|
+
'</body></html>',
|
|
1064
|
+
].join('\n')
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
const html = await server.transformIndexHtml(req.url, rawHtml)
|
|
1068
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
1069
|
+
res.end(html)
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
console.error('[storyboard] Component isolate HTML transform failed:', err)
|
|
1072
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
1073
|
+
res.end('Component isolate failed')
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
// ── Stories list API ──────────────────────────────────────────
|
|
1078
|
+
// Serves the list of discovered stories for the CLI and UI story picker.
|
|
1079
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1080
|
+
if (!req.url) return next()
|
|
1081
|
+
let url = req.url
|
|
1082
|
+
const baseNoTrail = (server.config.base || '/').replace(/\/$/, '')
|
|
1083
|
+
if (baseNoTrail && url.startsWith(baseNoTrail)) {
|
|
1084
|
+
url = url.slice(baseNoTrail.length) || '/'
|
|
1085
|
+
}
|
|
1086
|
+
if (!url.startsWith('/_storyboard/stories/list')) return next()
|
|
1087
|
+
|
|
1088
|
+
if (!buildResult) buildResult = buildIndex(root)
|
|
1089
|
+
const storyEntries = Object.entries(buildResult.index.story || {})
|
|
1090
|
+
const storyRoutes = buildResult.storyRoutes || {}
|
|
1091
|
+
const stories = storyEntries.map(([name]) => ({
|
|
1092
|
+
name,
|
|
1093
|
+
route: storyRoutes[name] || null,
|
|
1094
|
+
}))
|
|
1095
|
+
|
|
1096
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
1097
|
+
res.end(JSON.stringify({ stories }))
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
// Watch for data file changes in dev mode
|
|
1101
|
+
const watcher = server.watcher
|
|
1102
|
+
if (!buildResult) buildResult = buildIndex(root)
|
|
1103
|
+
const knownCanvasIds = new Set(Object.keys(buildResult.index.canvas || {}))
|
|
1104
|
+
const pendingCanvasUnlinks = new Map()
|
|
1105
|
+
|
|
1106
|
+
const triggerFullReload = () => {
|
|
1107
|
+
buildResult = null
|
|
1108
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1109
|
+
if (mod) {
|
|
1110
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1111
|
+
server.ws.send({ type: 'full-reload' })
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Mark the virtual module as stale so the next page load rebuilds it,
|
|
1116
|
+
// but do NOT trigger a full-reload (avoids losing canvas editing state).
|
|
1117
|
+
const softInvalidate = () => {
|
|
1118
|
+
buildResult = null
|
|
1119
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1120
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Read a canvas file and build HMR metadata for the client-side listener.
|
|
1124
|
+
const readCanvasMetadata = (filePath, parsed) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const absPath = path.resolve(root, filePath)
|
|
1127
|
+
const raw = fs.readFileSync(absPath, 'utf-8')
|
|
1128
|
+
const materialized = materializeFromText(raw)
|
|
1129
|
+
const result = { ...materialized }
|
|
1130
|
+
// Inject _route and _folder the same way generateModule does
|
|
1131
|
+
if (parsed.inferredRoute) result._route = parsed.inferredRoute
|
|
1132
|
+
const folderDirMatch = path.relative(root, absPath).replace(/\\/g, '/').match(/(?:^|\/)src\/(?:prototypes|canvas)\/([^/]+)\.folder\//)
|
|
1133
|
+
if (folderDirMatch) result._folder = folderDirMatch[1]
|
|
1134
|
+
return result
|
|
1135
|
+
} catch {
|
|
1136
|
+
return null
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const invalidate = (filePath) => {
|
|
1141
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
1142
|
+
// Canvas .jsonl content changes are mutated at runtime by the canvas
|
|
1143
|
+
// server API. A full-reload would create a feedback loop (save →
|
|
1144
|
+
// file change → reload → lose editing state). Instead, soft-invalidate
|
|
1145
|
+
// the virtual module (so page refresh picks up changes) and send a
|
|
1146
|
+
// custom HMR event with updated metadata so the canvas page and
|
|
1147
|
+
// viewfinder can react in place.
|
|
1148
|
+
if (/\.canvas\.jsonl$/.test(normalized)) {
|
|
1149
|
+
// If this file change was caused by the canvas server API, it has
|
|
1150
|
+
// already pushed an HMR event via pushCanvasUpdate(). Skip the
|
|
1151
|
+
// duplicate watcher-triggered event to prevent stale-data rollbacks.
|
|
1152
|
+
const absPath = path.resolve(root, filePath)
|
|
1153
|
+
if (!isCanvasWriteInFlight(absPath)) {
|
|
1154
|
+
const parsed = parseDataFile(filePath)
|
|
1155
|
+
if (parsed?.suffix === 'canvas' && parsed?.id) {
|
|
1156
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1157
|
+
server.ws.send({
|
|
1158
|
+
type: 'custom',
|
|
1159
|
+
event: 'storyboard:canvas-file-changed',
|
|
1160
|
+
data: { canvasId: parsed.id, name: parsed.id, ...(metadata ? { metadata } : {}) },
|
|
1161
|
+
})
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
softInvalidate()
|
|
1165
|
+
return
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Invalidate when any config file inside a prototype changes
|
|
1169
|
+
const protoConfigPattern = /\/(toolbar|commandpalette|widgets|paste)\.config\.json$/
|
|
1170
|
+
if (protoConfigPattern.test(normalized) && normalized.includes('/prototypes/')) {
|
|
1171
|
+
buildResult = null
|
|
1172
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1173
|
+
if (mod) {
|
|
1174
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1175
|
+
server.ws.send({ type: 'full-reload' })
|
|
1176
|
+
}
|
|
1177
|
+
return
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Invalidate when root toolbar.config.json changes
|
|
1181
|
+
if (normalized === path.resolve(root, 'toolbar.config.json').split(path.sep).join('/') ||
|
|
1182
|
+
normalized === path.resolve(root, 'toolbar.config.json')) {
|
|
1183
|
+
buildResult = null
|
|
1184
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1185
|
+
if (mod) {
|
|
1186
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1187
|
+
server.ws.send({ type: 'full-reload' })
|
|
1188
|
+
}
|
|
1189
|
+
return
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const parsed = parseDataFile(filePath)
|
|
1193
|
+
// Also invalidate when files are added/removed inside .folder/ directories
|
|
1194
|
+
const inFolder = normalized.includes('.folder/')
|
|
1195
|
+
if (!parsed && !inFolder) return
|
|
1196
|
+
// Source files inside .folder/ dirs (jsx, css, etc.) are handled by
|
|
1197
|
+
// Vite's built-in HMR / React Fast Refresh — don't full-reload for them.
|
|
1198
|
+
if (!parsed && inFolder) return
|
|
1199
|
+
|
|
1200
|
+
// Story file content changes are handled by Vite's built-in HMR
|
|
1201
|
+
// (React Fast Refresh). Only soft-invalidate the virtual module so
|
|
1202
|
+
// the next page load picks up updated metadata — don't full-reload,
|
|
1203
|
+
// which would destroy canvas state and cause embedded iframes to
|
|
1204
|
+
// reload unnecessarily.
|
|
1205
|
+
if (parsed?.suffix === 'story') {
|
|
1206
|
+
softInvalidate()
|
|
1207
|
+
return
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Rebuild index and invalidate virtual module
|
|
1211
|
+
buildResult = null
|
|
1212
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1213
|
+
if (mod) {
|
|
1214
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1215
|
+
server.ws.send({ type: 'full-reload' })
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const invalidateOnAddRemove = (filePath, eventType) => {
|
|
1220
|
+
const parsed = parseDataFile(filePath)
|
|
1221
|
+
const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
|
|
1222
|
+
if (!parsed && !inFolder) return
|
|
1223
|
+
// Source files (jsx, css, etc.) inside .folder/ dirs are handled by
|
|
1224
|
+
// Vite's built-in HMR — don't trigger a full-reload for them.
|
|
1225
|
+
if (!parsed && inFolder) return
|
|
1226
|
+
|
|
1227
|
+
// Canvas writers/editors can emit unlink+add for an in-place save.
|
|
1228
|
+
// Treat canvas add/unlink as runtime data updates and never full-reload
|
|
1229
|
+
// from watcher events. Canvas pages sync from disk via custom WS events.
|
|
1230
|
+
if (parsed?.suffix === 'canvas') {
|
|
1231
|
+
const canvasId = parsed.id || parsed.name
|
|
1232
|
+
if (eventType === 'unlink') {
|
|
1233
|
+
const timer = setTimeout(() => {
|
|
1234
|
+
pendingCanvasUnlinks.delete(canvasId)
|
|
1235
|
+
knownCanvasIds.delete(canvasId)
|
|
1236
|
+
server.ws.send({
|
|
1237
|
+
type: 'custom',
|
|
1238
|
+
event: 'storyboard:canvas-file-changed',
|
|
1239
|
+
data: { canvasId, name: canvasId, removed: true },
|
|
1240
|
+
})
|
|
1241
|
+
softInvalidate()
|
|
1242
|
+
}, 1500)
|
|
1243
|
+
pendingCanvasUnlinks.set(canvasId, timer)
|
|
1244
|
+
return
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (eventType === 'add') {
|
|
1248
|
+
const metadata = readCanvasMetadata(filePath, parsed)
|
|
1249
|
+
const pending = pendingCanvasUnlinks.get(canvasId)
|
|
1250
|
+
if (pending) {
|
|
1251
|
+
// unlink+add pair = in-place save (atomic write), not a real remove
|
|
1252
|
+
clearTimeout(pending)
|
|
1253
|
+
pendingCanvasUnlinks.delete(canvasId)
|
|
1254
|
+
server.ws.send({
|
|
1255
|
+
type: 'custom',
|
|
1256
|
+
event: 'storyboard:canvas-file-changed',
|
|
1257
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1258
|
+
})
|
|
1259
|
+
softInvalidate()
|
|
1260
|
+
return
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (knownCanvasIds.has(canvasId)) {
|
|
1264
|
+
server.ws.send({
|
|
1265
|
+
type: 'custom',
|
|
1266
|
+
event: 'storyboard:canvas-file-changed',
|
|
1267
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1268
|
+
})
|
|
1269
|
+
softInvalidate()
|
|
1270
|
+
return
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
knownCanvasIds.add(canvasId)
|
|
1274
|
+
server.ws.send({
|
|
1275
|
+
type: 'custom',
|
|
1276
|
+
event: 'storyboard:canvas-file-changed',
|
|
1277
|
+
data: { canvasId, name: canvasId, ...(metadata ? { metadata } : {}) },
|
|
1278
|
+
})
|
|
1279
|
+
softInvalidate()
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Story add/remove: soft-invalidate + custom HMR event (full-reload
|
|
1285
|
+
// is blocked by the canvas reload guard). The virtual module HMR
|
|
1286
|
+
// handler live-patches `stories` and re-runs init().
|
|
1287
|
+
if (parsed?.suffix === 'story') {
|
|
1288
|
+
softInvalidate()
|
|
1289
|
+
if (!buildResult) buildResult = buildIndex(root)
|
|
1290
|
+
const storyRoutes = buildResult.storyRoutes || {}
|
|
1291
|
+
const storyIndex = buildResult.index.story || {}
|
|
1292
|
+
const name = parsed.name
|
|
1293
|
+
if (eventType === 'unlink') {
|
|
1294
|
+
server.ws.send({
|
|
1295
|
+
type: 'custom',
|
|
1296
|
+
event: 'storyboard:story-file-changed',
|
|
1297
|
+
data: { name, removed: true },
|
|
1298
|
+
})
|
|
1299
|
+
} else if (eventType === 'add' && storyIndex[name]) {
|
|
1300
|
+
const relModule = '/' + path.relative(root, storyIndex[name]).replace(/\\/g, '/')
|
|
1301
|
+
server.ws.send({
|
|
1302
|
+
type: 'custom',
|
|
1303
|
+
event: 'storyboard:story-file-changed',
|
|
1304
|
+
data: {
|
|
1305
|
+
name,
|
|
1306
|
+
_storyModule: relModule,
|
|
1307
|
+
_route: storyRoutes[name] || null,
|
|
1308
|
+
},
|
|
1309
|
+
})
|
|
1310
|
+
}
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Non-canvas additions/removals and folder changes update the route/data graph.
|
|
1315
|
+
triggerFullReload()
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Watch storyboard.config.json for changes
|
|
1319
|
+
const { configPath } = readConfig(root)
|
|
1320
|
+
watcher.add(configPath)
|
|
1321
|
+
|
|
1322
|
+
// Watch all root domain config files for changes
|
|
1323
|
+
const domainConfigFiles = [
|
|
1324
|
+
'toolbar.config.json',
|
|
1325
|
+
'commandpalette.config.json',
|
|
1326
|
+
'paste.config.json',
|
|
1327
|
+
'widgets.config.json',
|
|
1328
|
+
].map(f => path.resolve(root, f))
|
|
1329
|
+
const watchedConfigPaths = new Set([configPath, ...domainConfigFiles])
|
|
1330
|
+
for (const p of domainConfigFiles) watcher.add(p)
|
|
1331
|
+
|
|
1332
|
+
const invalidateConfig = (filePath) => {
|
|
1333
|
+
const resolved = path.resolve(filePath)
|
|
1334
|
+
if (watchedConfigPaths.has(resolved)) {
|
|
1335
|
+
buildResult = null
|
|
1336
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
1337
|
+
if (mod) {
|
|
1338
|
+
server.moduleGraph.invalidateModule(mod)
|
|
1339
|
+
server.ws.send({ type: 'full-reload' })
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
watcher.on('add', (filePath) => invalidateOnAddRemove(filePath, 'add'))
|
|
1345
|
+
watcher.on('unlink', (filePath) => invalidateOnAddRemove(filePath, 'unlink'))
|
|
1346
|
+
watcher.on('change', (filePath) => {
|
|
1347
|
+
invalidate(filePath)
|
|
1348
|
+
invalidateConfig(filePath)
|
|
1349
|
+
})
|
|
1350
|
+
},
|
|
1351
|
+
|
|
1352
|
+
handleHotUpdate(ctx) {
|
|
1353
|
+
const normalized = ctx.file.replace(/\\/g, '/')
|
|
1354
|
+
if (!/\.canvas\.jsonl$/.test(normalized)) return
|
|
1355
|
+
|
|
1356
|
+
// Prevent Vite's default fallback behavior (full page reload) for
|
|
1357
|
+
// non-module .canvas.jsonl edits. The watcher 'change' handler
|
|
1358
|
+
// (invalidate) already sends the custom HMR event and soft-invalidates
|
|
1359
|
+
// the virtual module — no duplicate event needed here.
|
|
1360
|
+
return []
|
|
1361
|
+
},
|
|
1362
|
+
|
|
1363
|
+
// Inject __SB_BRANCHES__ into HTML so the Viewfinder branch selector works.
|
|
1364
|
+
// Uses server registry (live running processes) instead of stale ports.json.
|
|
1365
|
+
transformIndexHtml(html, ctx) {
|
|
1366
|
+
// Only inject in dev mode
|
|
1367
|
+
if (!ctx.server) return html
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
const servers = listRunningServers()
|
|
1371
|
+
const branches = servers
|
|
1372
|
+
.filter(srv => srv.worktree !== 'main')
|
|
1373
|
+
.map(srv => ({ branch: srv.worktree, folder: `branch--${srv.worktree}`, port: srv.port }))
|
|
1374
|
+
|
|
1375
|
+
if (branches.length === 0) return html
|
|
1376
|
+
|
|
1377
|
+
const script = `<script>window.__SB_BRANCHES__ = ${JSON.stringify(branches)};</script>`
|
|
1378
|
+
return html.replace('</head>', `${script}\n</head>`)
|
|
1379
|
+
} catch {
|
|
1380
|
+
return html
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
// Rebuild index on each build start
|
|
1385
|
+
buildStart() {
|
|
1386
|
+
buildResult = null
|
|
1387
|
+
},
|
|
1388
|
+
|
|
1389
|
+
// Emit terminal snapshots into the build so TerminalReadWidget can
|
|
1390
|
+
// fetch them as static files in production (no dev-server API).
|
|
1391
|
+
generateBundle() {
|
|
1392
|
+
const emittedIds = new Set()
|
|
1393
|
+
|
|
1394
|
+
// 1. New public snapshots (flat structure) — .json and .txt
|
|
1395
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1396
|
+
if (fs.existsSync(publicDir)) {
|
|
1397
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1398
|
+
if (file.startsWith('~') || file.startsWith('.')) continue
|
|
1399
|
+
const isJson = file.endsWith('.snapshot.json')
|
|
1400
|
+
const isTxt = file.endsWith('.snapshot.txt')
|
|
1401
|
+
if (!isJson && !isTxt) continue
|
|
1402
|
+
if (isJson) {
|
|
1403
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1404
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1405
|
+
}
|
|
1406
|
+
this.emitFile({
|
|
1407
|
+
type: 'asset',
|
|
1408
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1409
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1410
|
+
})
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1415
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1416
|
+
if (fs.existsSync(legacyDir)) {
|
|
1417
|
+
const walk = (dir) => {
|
|
1418
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1419
|
+
for (const entry of entries) {
|
|
1420
|
+
const full = path.join(dir, entry.name)
|
|
1421
|
+
if (entry.isDirectory()) {
|
|
1422
|
+
walk(full)
|
|
1423
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1424
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1425
|
+
if (emittedIds.has(widgetId)) continue
|
|
1426
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1427
|
+
this.emitFile({
|
|
1428
|
+
type: 'asset',
|
|
1429
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1430
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1431
|
+
})
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
walk(legacyDir)
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Vite plugin that copies terminal snapshots into the build output
|
|
1443
|
+
* so TerminalReadWidget can fetch them as static files in production.
|
|
1444
|
+
*
|
|
1445
|
+
* Sources (in priority order):
|
|
1446
|
+
* 1. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.json (new, flat)
|
|
1447
|
+
* 2. assets/.storyboard-public/terminal-snapshots/<widgetId>.snapshot.txt (human-readable companion)
|
|
1448
|
+
* 3. .storyboard/terminal-snapshots/<canvasDir>/<widgetId>.json (legacy, nested)
|
|
1449
|
+
*
|
|
1450
|
+
* All are emitted to `_storyboard/terminal-snapshots/` in the build.
|
|
1451
|
+
* Tilde-prefixed files (~) are excluded (private).
|
|
1452
|
+
*/
|
|
1453
|
+
export function terminalSnapshotPlugin() {
|
|
1454
|
+
return {
|
|
1455
|
+
name: 'storyboard-terminal-snapshots',
|
|
1456
|
+
|
|
1457
|
+
generateBundle() {
|
|
1458
|
+
const emittedIds = new Set()
|
|
1459
|
+
|
|
1460
|
+
// 1. New public snapshots (flat structure) — .json and .txt
|
|
1461
|
+
const publicDir = path.resolve('assets/.storyboard-public/terminal-snapshots')
|
|
1462
|
+
if (fs.existsSync(publicDir)) {
|
|
1463
|
+
for (const file of fs.readdirSync(publicDir)) {
|
|
1464
|
+
if (file.startsWith('~') || file.startsWith('.')) continue
|
|
1465
|
+
const isJson = file.endsWith('.snapshot.json')
|
|
1466
|
+
const isTxt = file.endsWith('.snapshot.txt')
|
|
1467
|
+
if (!isJson && !isTxt) continue
|
|
1468
|
+
if (isJson) {
|
|
1469
|
+
const widgetId = file.replace(/\.snapshot\.json$/, '')
|
|
1470
|
+
if (widgetId) emittedIds.add(widgetId)
|
|
1471
|
+
}
|
|
1472
|
+
this.emitFile({
|
|
1473
|
+
type: 'asset',
|
|
1474
|
+
fileName: `_storyboard/terminal-snapshots/${file}`,
|
|
1475
|
+
source: fs.readFileSync(path.join(publicDir, file), 'utf-8'),
|
|
1476
|
+
})
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// 2. Legacy snapshots (nested by canvas dir) — skip if already emitted
|
|
1481
|
+
const legacyDir = path.resolve('.storyboard/terminal-snapshots')
|
|
1482
|
+
if (fs.existsSync(legacyDir)) {
|
|
1483
|
+
const walk = (dir) => {
|
|
1484
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1485
|
+
for (const entry of entries) {
|
|
1486
|
+
const full = path.join(dir, entry.name)
|
|
1487
|
+
if (entry.isDirectory()) {
|
|
1488
|
+
walk(full)
|
|
1489
|
+
} else if (entry.name.endsWith('.json') && !entry.name.startsWith('~')) {
|
|
1490
|
+
const widgetId = entry.name.replace(/\.json$/, '')
|
|
1491
|
+
if (emittedIds.has(widgetId)) continue // new format takes priority
|
|
1492
|
+
const rel = path.relative(legacyDir, full).replace(/\\/g, '/')
|
|
1493
|
+
this.emitFile({
|
|
1494
|
+
type: 'asset',
|
|
1495
|
+
fileName: `_storyboard/terminal-snapshots/${rel}`,
|
|
1496
|
+
source: fs.readFileSync(full, 'utf-8'),
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
walk(legacyDir)
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Exported for testing
|
|
1508
|
+
export { resolveTemplateVars, computeTemplateVars, parseDataFile }
|