proto-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// React Component Name Detection
|
|
3
|
+
// Uses React DevTools techniques to extract component names from fiber nodes
|
|
4
|
+
// =============================================================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Fiber node type (minimal subset we care about)
|
|
8
|
+
* Based on React internal structure
|
|
9
|
+
*/
|
|
10
|
+
interface ReactFiber {
|
|
11
|
+
tag: number;
|
|
12
|
+
type: ComponentType | string | null;
|
|
13
|
+
elementType: ComponentType | null;
|
|
14
|
+
return: ReactFiber | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ComponentType {
|
|
18
|
+
name?: string;
|
|
19
|
+
displayName?: string;
|
|
20
|
+
render?: { name?: string; displayName?: string };
|
|
21
|
+
type?: ComponentType;
|
|
22
|
+
_context?: { displayName?: string };
|
|
23
|
+
_status?: number;
|
|
24
|
+
_result?: ComponentType;
|
|
25
|
+
$$typeof?: symbol;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fiber tags from React source (stable across versions)
|
|
30
|
+
* https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
|
|
31
|
+
*/
|
|
32
|
+
const FiberTags = {
|
|
33
|
+
FunctionComponent: 0,
|
|
34
|
+
ClassComponent: 1,
|
|
35
|
+
IndeterminateComponent: 2,
|
|
36
|
+
HostRoot: 3,
|
|
37
|
+
HostPortal: 4,
|
|
38
|
+
HostComponent: 5, // DOM elements like <div>
|
|
39
|
+
HostText: 6,
|
|
40
|
+
Fragment: 7,
|
|
41
|
+
Mode: 8,
|
|
42
|
+
ContextConsumer: 9,
|
|
43
|
+
ContextProvider: 10,
|
|
44
|
+
ForwardRef: 11,
|
|
45
|
+
Profiler: 12,
|
|
46
|
+
SuspenseComponent: 13,
|
|
47
|
+
MemoComponent: 14,
|
|
48
|
+
SimpleMemoComponent: 15,
|
|
49
|
+
LazyComponent: 16,
|
|
50
|
+
// React 18/19 additions
|
|
51
|
+
IncompleteClassComponent: 17,
|
|
52
|
+
DehydratedFragment: 18,
|
|
53
|
+
SuspenseListComponent: 19,
|
|
54
|
+
// Note: 20 is unused/reserved
|
|
55
|
+
ScopeComponent: 21,
|
|
56
|
+
OffscreenComponent: 22,
|
|
57
|
+
LegacyHiddenComponent: 23,
|
|
58
|
+
CacheComponent: 24,
|
|
59
|
+
TracingMarkerComponent: 25,
|
|
60
|
+
HostHoistable: 26,
|
|
61
|
+
HostSingleton: 27,
|
|
62
|
+
IncompleteFunctionComponent: 28,
|
|
63
|
+
Throw: 29,
|
|
64
|
+
ViewTransitionComponent: 30,
|
|
65
|
+
ActivityComponent: 31,
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// Default Filter Configuration
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default exact names to always skip (React internals)
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_SKIP_EXACT = new Set([
|
|
76
|
+
"Component",
|
|
77
|
+
"PureComponent",
|
|
78
|
+
"Fragment",
|
|
79
|
+
"Suspense",
|
|
80
|
+
"Profiler",
|
|
81
|
+
"StrictMode",
|
|
82
|
+
"Routes",
|
|
83
|
+
"Route",
|
|
84
|
+
"Outlet",
|
|
85
|
+
// Framework internals - exact matches
|
|
86
|
+
"Root",
|
|
87
|
+
"ErrorBoundaryHandler",
|
|
88
|
+
"HotReload",
|
|
89
|
+
"Hot",
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Default patterns for framework internals
|
|
94
|
+
* Note: Patterns are designed to be specific to avoid false positives
|
|
95
|
+
* (e.g., ServerStatus, ClientProfile should NOT be filtered)
|
|
96
|
+
*/
|
|
97
|
+
export const DEFAULT_SKIP_PATTERNS: RegExp[] = [
|
|
98
|
+
/Boundary$/, // ErrorBoundary, RedirectBoundary
|
|
99
|
+
/BoundaryHandler$/, // ErrorBoundaryHandler
|
|
100
|
+
/Provider$/, // ThemeProvider, Context.Provider
|
|
101
|
+
/Consumer$/, // Context.Consumer
|
|
102
|
+
/^(Inner|Outer)/, // InnerLayoutRouter
|
|
103
|
+
/Router$/, // AppRouter, BrowserRouter
|
|
104
|
+
/^Client(Page|Segment|Root)/, // ClientPageRoot, ClientSegmentRoot
|
|
105
|
+
/^Segment(ViewNode|Node)$/, // Next.js App Router internals
|
|
106
|
+
/^LayoutSegment/, // Next.js layout segment wrappers
|
|
107
|
+
/^Server(Root|Component|Render)/, // ServerRoot (not ServerStatus)
|
|
108
|
+
/^RSC/, // RSCComponent
|
|
109
|
+
/Context$/, // LayoutRouterContext
|
|
110
|
+
/^Hot(Reload)?$/, // HotReload (exact match to avoid false positives)
|
|
111
|
+
/^(Dev|React)(Overlay|Tools|Root)/, // DevTools, ReactDevOverlay
|
|
112
|
+
/Overlay$/, // ReactDevOverlay, ErrorOverlay
|
|
113
|
+
/Handler$/, // ScrollAndFocusHandler, ErrorBoundaryHandler
|
|
114
|
+
/^With[A-Z]/, // withRouter, WithAuth (HOCs)
|
|
115
|
+
/Wrapper$/, // Generic wrappers
|
|
116
|
+
/^Root$/, // Generic Root component
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Patterns that indicate likely user-defined components
|
|
121
|
+
* Used as fallback in 'smart' mode
|
|
122
|
+
*/
|
|
123
|
+
const DEFAULT_USER_PATTERNS: RegExp[] = [
|
|
124
|
+
/Page$/, // HomePage, InstallPage
|
|
125
|
+
/View$/, // ListView, DetailView
|
|
126
|
+
/Screen$/, // HomeScreen
|
|
127
|
+
/Section$/, // HeroSection
|
|
128
|
+
/Card$/, // ProductCard
|
|
129
|
+
/List$/, // UserList
|
|
130
|
+
/Item$/, // ListItem, MenuItem
|
|
131
|
+
/Form$/, // LoginForm
|
|
132
|
+
/Modal$/, // ConfirmModal
|
|
133
|
+
/Dialog$/, // AlertDialog
|
|
134
|
+
/Button$/, // SubmitButton (but not all buttons)
|
|
135
|
+
/Nav$/, // SideNav, TopNav
|
|
136
|
+
/Header$/, // PageHeader
|
|
137
|
+
/Footer$/, // PageFooter
|
|
138
|
+
/Layout$/, // MainLayout (careful - could be framework)
|
|
139
|
+
/Panel$/, // SidePanel
|
|
140
|
+
/Tab$/, // SettingsTab
|
|
141
|
+
/Menu$/, // DropdownMenu
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Configuration Types
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
export type ReactDetectionMode = "all" | "filtered" | "smart";
|
|
149
|
+
|
|
150
|
+
export interface ReactDetectionConfig {
|
|
151
|
+
/**
|
|
152
|
+
* How many component names to collect
|
|
153
|
+
* @default 3
|
|
154
|
+
*/
|
|
155
|
+
maxComponents?: number;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Maximum fiber depth to traverse
|
|
159
|
+
* @default 25
|
|
160
|
+
*/
|
|
161
|
+
maxDepth?: number;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Detection mode:
|
|
165
|
+
* - 'smart': Only show components that correlate with DOM classes (strictest, most relevant)
|
|
166
|
+
* - 'filtered': Skip known framework internals (default)
|
|
167
|
+
* - 'all': Show all components (no filtering)
|
|
168
|
+
* @default 'filtered'
|
|
169
|
+
*/
|
|
170
|
+
mode?: ReactDetectionMode;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Additional exact names to skip (merged with defaults in 'filtered' mode)
|
|
174
|
+
*/
|
|
175
|
+
skipExact?: Set<string> | string[];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Additional patterns to skip (merged with defaults in 'filtered' mode)
|
|
179
|
+
*/
|
|
180
|
+
skipPatterns?: RegExp[];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Patterns for user components (used as fallback in 'smart' mode)
|
|
184
|
+
*/
|
|
185
|
+
userPatterns?: RegExp[];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Custom filter function for full control
|
|
189
|
+
* Return true to INCLUDE the component, false to skip
|
|
190
|
+
*/
|
|
191
|
+
filter?: (name: string, depth: number) => boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolved configuration with all defaults applied
|
|
196
|
+
*/
|
|
197
|
+
interface ResolvedConfig {
|
|
198
|
+
maxComponents: number;
|
|
199
|
+
maxDepth: number;
|
|
200
|
+
mode: ReactDetectionMode;
|
|
201
|
+
skipExact: Set<string>;
|
|
202
|
+
skipPatterns: RegExp[];
|
|
203
|
+
userPatterns: RegExp[];
|
|
204
|
+
filter?: (name: string, depth: number) => boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveConfig(config?: ReactDetectionConfig): ResolvedConfig {
|
|
208
|
+
const mode = config?.mode ?? "filtered";
|
|
209
|
+
|
|
210
|
+
// Convert skipExact to Set if array
|
|
211
|
+
let skipExact = DEFAULT_SKIP_EXACT;
|
|
212
|
+
if (config?.skipExact) {
|
|
213
|
+
const additional =
|
|
214
|
+
config.skipExact instanceof Set
|
|
215
|
+
? config.skipExact
|
|
216
|
+
: new Set(config.skipExact);
|
|
217
|
+
skipExact = new Set([...DEFAULT_SKIP_EXACT, ...additional]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
maxComponents: config?.maxComponents ?? 6,
|
|
222
|
+
maxDepth: config?.maxDepth ?? 30,
|
|
223
|
+
mode,
|
|
224
|
+
skipExact,
|
|
225
|
+
skipPatterns: config?.skipPatterns
|
|
226
|
+
? [...DEFAULT_SKIP_PATTERNS, ...config.skipPatterns]
|
|
227
|
+
: DEFAULT_SKIP_PATTERNS,
|
|
228
|
+
userPatterns: config?.userPatterns ?? DEFAULT_USER_PATTERNS,
|
|
229
|
+
filter: config?.filter,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Filter Logic
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Normalize a component name to match CSS class conventions
|
|
239
|
+
* SideNav -> side-nav, LinkComponent -> link-component
|
|
240
|
+
*/
|
|
241
|
+
function normalizeComponentName(name: string): string {
|
|
242
|
+
return name
|
|
243
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
244
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
|
245
|
+
.toLowerCase();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Collect CSS classes from an element and its ancestors
|
|
250
|
+
*/
|
|
251
|
+
function getAncestorClasses(element: HTMLElement, maxDepth = 10): Set<string> {
|
|
252
|
+
const classes = new Set<string>();
|
|
253
|
+
let current: HTMLElement | null = element;
|
|
254
|
+
let depth = 0;
|
|
255
|
+
|
|
256
|
+
while (current && depth < maxDepth) {
|
|
257
|
+
if (current.className && typeof current.className === "string") {
|
|
258
|
+
current.className.split(/\s+/).forEach((cls) => {
|
|
259
|
+
if (cls.length > 1) {
|
|
260
|
+
// Normalize: remove CSS module hashes, convert to lowercase
|
|
261
|
+
const normalized = cls
|
|
262
|
+
.replace(/[_][a-zA-Z0-9]{5,}.*$/, "")
|
|
263
|
+
.toLowerCase();
|
|
264
|
+
if (normalized.length > 1) {
|
|
265
|
+
classes.add(normalized);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
current = current.parentElement;
|
|
271
|
+
depth++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return classes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a component name correlates with any DOM class
|
|
279
|
+
*/
|
|
280
|
+
function componentCorrelatesWithDOM(
|
|
281
|
+
componentName: string,
|
|
282
|
+
domClasses: Set<string>,
|
|
283
|
+
): boolean {
|
|
284
|
+
const normalized = normalizeComponentName(componentName);
|
|
285
|
+
|
|
286
|
+
for (const cls of domClasses) {
|
|
287
|
+
// Exact match: SideNav -> side-nav
|
|
288
|
+
if (cls === normalized) return true;
|
|
289
|
+
|
|
290
|
+
// Contains match: LinkComponent -> nav-link contains "link"
|
|
291
|
+
// Split both by hyphens and check for word overlaps
|
|
292
|
+
const componentWords = normalized.split("-").filter((w) => w.length > 2);
|
|
293
|
+
const classWords = cls.split("-").filter((w) => w.length > 2);
|
|
294
|
+
|
|
295
|
+
for (const cWord of componentWords) {
|
|
296
|
+
for (const dWord of classWords) {
|
|
297
|
+
if (cWord === dWord || cWord.includes(dWord) || dWord.includes(cWord)) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function shouldIncludeComponent(
|
|
308
|
+
name: string,
|
|
309
|
+
depth: number,
|
|
310
|
+
config: ResolvedConfig,
|
|
311
|
+
domClasses?: Set<string>,
|
|
312
|
+
): boolean {
|
|
313
|
+
// Custom filter takes precedence
|
|
314
|
+
if (config.filter) {
|
|
315
|
+
return config.filter(name, depth);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
switch (config.mode) {
|
|
319
|
+
case "all":
|
|
320
|
+
// "all" mode shows everything - no filtering at all
|
|
321
|
+
return true;
|
|
322
|
+
|
|
323
|
+
case "filtered":
|
|
324
|
+
// "filtered" mode skips framework internals
|
|
325
|
+
if (config.skipExact.has(name)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
if (config.skipPatterns.some((p) => p.test(name))) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
|
|
333
|
+
case "smart":
|
|
334
|
+
// "smart" mode: first apply framework filters, then require DOM correlation
|
|
335
|
+
if (config.skipExact.has(name)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
if (config.skipPatterns.some((p) => p.test(name))) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
// Must correlate with DOM classes OR match user patterns
|
|
342
|
+
if (domClasses && componentCorrelatesWithDOM(name, domClasses)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (config.userPatterns.some((p) => p.test(name))) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// Skip components that don't correlate - this mode is intentionally strict
|
|
349
|
+
return false;
|
|
350
|
+
|
|
351
|
+
default:
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// =============================================================================
|
|
357
|
+
// React Detection
|
|
358
|
+
// =============================================================================
|
|
359
|
+
|
|
360
|
+
let reactDetectionCache: boolean | null = null;
|
|
361
|
+
|
|
362
|
+
// Only cache for 'all' mode - filtered modes should NOT cache because:
|
|
363
|
+
// 1. Filter results depend on config that may change between calls
|
|
364
|
+
// 2. Cached results from before filter changes would return stale/unfiltered data
|
|
365
|
+
// 3. The cache lookup happens BEFORE filtering, so old cached data bypasses filters
|
|
366
|
+
// Using WeakMap allows garbage collection when elements are removed from DOM.
|
|
367
|
+
const componentCacheAll = new WeakMap<HTMLElement, ReactComponentInfo>();
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Checks if React is present on the page
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* Check if an element has React fiber keys
|
|
374
|
+
*/
|
|
375
|
+
function hasReactFiber(element: Element): boolean {
|
|
376
|
+
return Object.keys(element).some(
|
|
377
|
+
(key) =>
|
|
378
|
+
key.startsWith("__reactFiber$") ||
|
|
379
|
+
key.startsWith("__reactInternalInstance$") ||
|
|
380
|
+
key.startsWith("__reactProps$"),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Checks if React is present on the page.
|
|
386
|
+
* Scans common React root containers since React typically mounts
|
|
387
|
+
* to #root, #app, #__next, etc. rather than document.body directly.
|
|
388
|
+
*/
|
|
389
|
+
export function isReactPage(): boolean {
|
|
390
|
+
if (reactDetectionCache !== null) {
|
|
391
|
+
return reactDetectionCache;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (typeof document === "undefined") {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check body first (some apps mount directly to body)
|
|
399
|
+
if (document.body && hasReactFiber(document.body)) {
|
|
400
|
+
reactDetectionCache = true;
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Check common React root containers
|
|
405
|
+
const commonRoots = ["#root", "#app", "#__next", "[data-reactroot]"];
|
|
406
|
+
for (const selector of commonRoots) {
|
|
407
|
+
const el = document.querySelector(selector);
|
|
408
|
+
if (el && hasReactFiber(el)) {
|
|
409
|
+
reactDetectionCache = true;
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Scan immediate children of body as fallback
|
|
415
|
+
if (document.body) {
|
|
416
|
+
for (const child of document.body.children) {
|
|
417
|
+
if (hasReactFiber(child)) {
|
|
418
|
+
reactDetectionCache = true;
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
reactDetectionCache = false;
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Wrapper object to allow cache clearing (WeakMap has no clear() method)
|
|
429
|
+
let componentCacheAllRef = { map: componentCacheAll };
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Clears the React detection cache
|
|
433
|
+
* Note: Only 'all' mode uses caching; filtered modes don't cache to avoid stale filter results
|
|
434
|
+
*/
|
|
435
|
+
export function clearReactDetectionCache(): void {
|
|
436
|
+
reactDetectionCache = null;
|
|
437
|
+
componentCacheAllRef.map = new WeakMap<HTMLElement, ReactComponentInfo>();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getReactFiberKey(element: HTMLElement): string | null {
|
|
441
|
+
const keys = Object.keys(element);
|
|
442
|
+
return (
|
|
443
|
+
keys.find(
|
|
444
|
+
(key) =>
|
|
445
|
+
key.startsWith("__reactFiber$") ||
|
|
446
|
+
key.startsWith("__reactInternalInstance$"),
|
|
447
|
+
) || null
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getFiberFromElement(element: HTMLElement): ReactFiber | null {
|
|
452
|
+
const key = getReactFiberKey(element);
|
|
453
|
+
if (!key) return null;
|
|
454
|
+
return (element as unknown as Record<string, unknown>)[
|
|
455
|
+
key
|
|
456
|
+
] as ReactFiber | null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function getComponentNameFromType(type: ComponentType | null): string | null {
|
|
460
|
+
if (!type) return null;
|
|
461
|
+
if (type.displayName) return type.displayName;
|
|
462
|
+
if (type.name) return type.name;
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getComponentNameFromFiber(fiber: ReactFiber): string | null {
|
|
467
|
+
const { tag, type, elementType } = fiber;
|
|
468
|
+
|
|
469
|
+
// Skip DOM elements and host types
|
|
470
|
+
if (
|
|
471
|
+
tag === FiberTags.HostComponent ||
|
|
472
|
+
tag === FiberTags.HostText ||
|
|
473
|
+
tag === FiberTags.HostHoistable ||
|
|
474
|
+
tag === FiberTags.HostSingleton
|
|
475
|
+
) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Skip Fragment, Mode, Profiler, and related internal types
|
|
480
|
+
if (
|
|
481
|
+
tag === FiberTags.Fragment ||
|
|
482
|
+
tag === FiberTags.Mode ||
|
|
483
|
+
tag === FiberTags.Profiler ||
|
|
484
|
+
tag === FiberTags.DehydratedFragment
|
|
485
|
+
) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Skip React internal infrastructure types (these are internal implementation details)
|
|
490
|
+
if (
|
|
491
|
+
tag === FiberTags.HostRoot ||
|
|
492
|
+
tag === FiberTags.HostPortal ||
|
|
493
|
+
tag === FiberTags.ScopeComponent ||
|
|
494
|
+
tag === FiberTags.OffscreenComponent ||
|
|
495
|
+
tag === FiberTags.LegacyHiddenComponent ||
|
|
496
|
+
tag === FiberTags.CacheComponent ||
|
|
497
|
+
tag === FiberTags.TracingMarkerComponent ||
|
|
498
|
+
tag === FiberTags.Throw ||
|
|
499
|
+
tag === FiberTags.ViewTransitionComponent ||
|
|
500
|
+
tag === FiberTags.ActivityComponent
|
|
501
|
+
) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Handle ForwardRef
|
|
506
|
+
if (tag === FiberTags.ForwardRef) {
|
|
507
|
+
const elType = elementType as ComponentType | null;
|
|
508
|
+
if (elType?.render) {
|
|
509
|
+
const innerName = getComponentNameFromType(elType.render);
|
|
510
|
+
if (innerName) return innerName;
|
|
511
|
+
}
|
|
512
|
+
if (elType?.displayName) return elType.displayName;
|
|
513
|
+
return getComponentNameFromType(type as ComponentType);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Handle Memo
|
|
517
|
+
if (
|
|
518
|
+
tag === FiberTags.MemoComponent ||
|
|
519
|
+
tag === FiberTags.SimpleMemoComponent
|
|
520
|
+
) {
|
|
521
|
+
const elType = elementType as ComponentType | null;
|
|
522
|
+
if (elType?.type) {
|
|
523
|
+
const innerName = getComponentNameFromType(elType.type);
|
|
524
|
+
if (innerName) return innerName;
|
|
525
|
+
}
|
|
526
|
+
if (elType?.displayName) return elType.displayName;
|
|
527
|
+
return getComponentNameFromType(type as ComponentType);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Handle Context Provider
|
|
531
|
+
if (tag === FiberTags.ContextProvider) {
|
|
532
|
+
const elType = type as ComponentType | null;
|
|
533
|
+
if (elType?._context?.displayName) {
|
|
534
|
+
return `${elType._context.displayName}.Provider`;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Handle Context Consumer
|
|
540
|
+
if (tag === FiberTags.ContextConsumer) {
|
|
541
|
+
const elType = type as ComponentType | null;
|
|
542
|
+
if (elType?.displayName) {
|
|
543
|
+
return `${elType.displayName}.Consumer`;
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Handle Lazy
|
|
549
|
+
if (tag === FiberTags.LazyComponent) {
|
|
550
|
+
const elType = elementType as ComponentType | null;
|
|
551
|
+
if (elType?._status === 1 && elType._result) {
|
|
552
|
+
return getComponentNameFromType(elType._result);
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Handle Suspense and SuspenseList
|
|
558
|
+
if (
|
|
559
|
+
tag === FiberTags.SuspenseComponent ||
|
|
560
|
+
tag === FiberTags.SuspenseListComponent
|
|
561
|
+
) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Handle incomplete components (error states during rendering)
|
|
566
|
+
if (
|
|
567
|
+
tag === FiberTags.IncompleteClassComponent ||
|
|
568
|
+
tag === FiberTags.IncompleteFunctionComponent
|
|
569
|
+
) {
|
|
570
|
+
// These are components that errored during rendering
|
|
571
|
+
// Try to get the name anyway for debugging purposes
|
|
572
|
+
return getComponentNameFromType(type as ComponentType);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Function and Class components
|
|
576
|
+
if (
|
|
577
|
+
tag === FiberTags.FunctionComponent ||
|
|
578
|
+
tag === FiberTags.ClassComponent ||
|
|
579
|
+
tag === FiberTags.IndeterminateComponent
|
|
580
|
+
) {
|
|
581
|
+
return getComponentNameFromType(type as ComponentType);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// =============================================================================
|
|
588
|
+
// Public API
|
|
589
|
+
// =============================================================================
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Result from React component detection
|
|
593
|
+
*/
|
|
594
|
+
export interface ReactComponentInfo {
|
|
595
|
+
/** Full component path like "<App> <Layout> <Button>" */
|
|
596
|
+
path: string | null;
|
|
597
|
+
/** Array of component names from innermost to outermost */
|
|
598
|
+
components: string[];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Check if a name looks like minified/production code (single letter or very short)
|
|
603
|
+
*/
|
|
604
|
+
function isMinifiedName(name: string): boolean {
|
|
605
|
+
// Single letter or two letters that look like minified (e.g., "e", "t", "Zt")
|
|
606
|
+
if (name.length <= 2) return true;
|
|
607
|
+
// All lowercase short names are likely minified
|
|
608
|
+
if (name.length <= 3 && name === name.toLowerCase()) return true;
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Walks up the fiber tree to collect React component names
|
|
614
|
+
*
|
|
615
|
+
* @param element - The DOM element to start from
|
|
616
|
+
* @param config - Optional configuration
|
|
617
|
+
* @returns ReactComponentInfo with component path and array
|
|
618
|
+
*/
|
|
619
|
+
export function getReactComponentName(
|
|
620
|
+
element: HTMLElement,
|
|
621
|
+
config?: ReactDetectionConfig,
|
|
622
|
+
): ReactComponentInfo {
|
|
623
|
+
const resolved = resolveConfig(config);
|
|
624
|
+
|
|
625
|
+
// Only use cache for 'all' mode - filtered modes must NOT cache because:
|
|
626
|
+
// - Cache lookup happens BEFORE filtering logic runs
|
|
627
|
+
// - Cached results from before filter updates would bypass new filters
|
|
628
|
+
// - This was causing "Root", "ErrorBoundaryHandler" to leak through
|
|
629
|
+
const useCache = resolved.mode === "all";
|
|
630
|
+
|
|
631
|
+
if (useCache) {
|
|
632
|
+
const cached = componentCacheAllRef.map.get(element);
|
|
633
|
+
if (cached !== undefined) {
|
|
634
|
+
return cached;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!isReactPage()) {
|
|
639
|
+
const result: ReactComponentInfo = { path: null, components: [] };
|
|
640
|
+
if (useCache) {
|
|
641
|
+
componentCacheAllRef.map.set(element, result);
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Collect DOM classes for smart mode
|
|
647
|
+
const domClasses =
|
|
648
|
+
resolved.mode === "smart" ? getAncestorClasses(element) : undefined;
|
|
649
|
+
|
|
650
|
+
const components: string[] = [];
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
let fiber = getFiberFromElement(element);
|
|
654
|
+
let depth = 0;
|
|
655
|
+
|
|
656
|
+
while (
|
|
657
|
+
fiber &&
|
|
658
|
+
depth < resolved.maxDepth &&
|
|
659
|
+
components.length < resolved.maxComponents
|
|
660
|
+
) {
|
|
661
|
+
const name = getComponentNameFromFiber(fiber);
|
|
662
|
+
|
|
663
|
+
// Skip minified names and apply filter
|
|
664
|
+
if (
|
|
665
|
+
name &&
|
|
666
|
+
!isMinifiedName(name) &&
|
|
667
|
+
shouldIncludeComponent(name, depth, resolved, domClasses)
|
|
668
|
+
) {
|
|
669
|
+
components.push(name);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fiber = fiber.return;
|
|
673
|
+
depth++;
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
// Fiber structure may be corrupted or inaccessible - return empty result
|
|
677
|
+
const result: ReactComponentInfo = { path: null, components: [] };
|
|
678
|
+
if (useCache) {
|
|
679
|
+
componentCacheAllRef.map.set(element, result);
|
|
680
|
+
}
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (components.length === 0) {
|
|
685
|
+
const result: ReactComponentInfo = { path: null, components: [] };
|
|
686
|
+
if (useCache) {
|
|
687
|
+
componentCacheAllRef.map.set(element, result);
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Build path from outermost to innermost: <App> <Layout> <Button>
|
|
693
|
+
const path = components
|
|
694
|
+
.slice()
|
|
695
|
+
.reverse()
|
|
696
|
+
.map((c) => `<${c}>`)
|
|
697
|
+
.join(" ");
|
|
698
|
+
|
|
699
|
+
const result: ReactComponentInfo = { path, components };
|
|
700
|
+
if (useCache) {
|
|
701
|
+
componentCacheAllRef.map.set(element, result);
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function commentToLiveState<TState>(
|
|
2
|
+
comment: TState & { capturedAt: string; route: string },
|
|
3
|
+
): TState {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
const { capturedAt: _capturedAt, route: _route, ...live } = comment as Record<string, unknown>;
|
|
6
|
+
return structuredClone(live) as TState;
|
|
7
|
+
}
|