design-clone 1.2.0 → 2.3.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.
Files changed (174) hide show
  1. package/README.md +32 -39
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -106
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +11 -56
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +11 -16
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +24 -28
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +556 -0
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +20 -21
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/{css-extractor.js → css/css-extractor.js} +4 -4
  51. package/src/core/css/filter-css-dead-code.js +120 -0
  52. package/src/core/css/filter-css-html-analyzer.js +110 -0
  53. package/src/core/css/filter-css-selector-matcher.js +172 -0
  54. package/src/core/css/filter-css.js +206 -0
  55. package/src/core/css/merge-css-atrule-processor.js +158 -0
  56. package/src/core/css/merge-css-file-io.js +68 -0
  57. package/src/core/css/merge-css.js +148 -0
  58. package/src/core/detection/framework-detector-routing.js +68 -0
  59. package/src/core/detection/framework-detector-signals.js +65 -0
  60. package/src/core/detection/framework-detector.js +198 -0
  61. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  62. package/src/core/dimension/dimension-extractor.js +317 -0
  63. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  64. package/src/core/dimension/dimension-output.js +173 -0
  65. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  66. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  67. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  68. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  69. package/src/core/discovery/app-state-snapshot.js +131 -0
  70. package/src/core/discovery/discover-pages-routes.js +84 -0
  71. package/src/core/discovery/discover-pages-utils.js +177 -0
  72. package/src/core/discovery/discover-pages.js +191 -0
  73. package/src/core/html/html-extractor-inline-styler.js +70 -0
  74. package/src/core/html/html-extractor.js +147 -0
  75. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  76. package/src/core/html/semantic-enhancer-page.js +148 -0
  77. package/src/core/html/semantic-enhancer.js +135 -0
  78. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  79. package/src/core/links/rewrite-links.js +173 -0
  80. package/src/core/media/asset-validator.js +118 -0
  81. package/src/core/media/extract-assets-downloader.js +187 -0
  82. package/src/core/media/extract-assets-page-scraper.js +115 -0
  83. package/src/core/media/extract-assets.js +159 -0
  84. package/src/core/media/video-capture-convert.js +200 -0
  85. package/src/core/media/video-capture.js +201 -0
  86. package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +1 -1
  87. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +44 -46
  88. package/src/core/{page-readiness.js → page-prep/page-readiness.js} +8 -8
  89. package/src/core/section/section-cropper-helpers.js +43 -0
  90. package/src/core/section/section-cropper.js +132 -0
  91. package/src/core/section/section-detector-strategies.js +139 -0
  92. package/src/core/section/section-detector-utils.js +100 -0
  93. package/src/core/section/section-detector.js +88 -0
  94. package/src/core/tests/test-section-cropper.js +177 -0
  95. package/src/core/tests/test-section-detector.js +55 -0
  96. package/src/post-process/enhance-assets.js +29 -4
  97. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  98. package/src/post-process/fetch-images.js +60 -263
  99. package/src/post-process/inject-gosnap.js +88 -0
  100. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  101. package/src/post-process/inject-icons.js +47 -200
  102. package/src/route-discoverers/angular-discoverer.js +157 -0
  103. package/src/route-discoverers/astro-discoverer.js +123 -0
  104. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  105. package/src/route-discoverers/base-discoverer.js +153 -0
  106. package/src/route-discoverers/index.js +106 -0
  107. package/src/route-discoverers/next-discoverer.js +130 -0
  108. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  109. package/src/route-discoverers/react-discoverer.js +139 -0
  110. package/src/route-discoverers/svelte-discoverer.js +109 -0
  111. package/src/route-discoverers/universal-discoverer.js +227 -0
  112. package/src/route-discoverers/vue-discoverer.js +118 -0
  113. package/src/shared/config.js +38 -0
  114. package/src/shared/error-codes.js +31 -0
  115. package/src/shared/viewports.js +46 -0
  116. package/src/utils/browser.js +11 -44
  117. package/src/utils/helpers.js +4 -0
  118. package/src/utils/log.js +12 -0
  119. package/src/utils/playwright-loader.js +76 -0
  120. package/src/utils/playwright.js +147 -0
  121. package/src/utils/progress.js +32 -0
  122. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  123. package/src/verification/generate-audit-report-sections.js +158 -0
  124. package/src/verification/generate-audit-report.js +122 -0
  125. package/src/verification/quality-scorer.js +92 -0
  126. package/src/verification/verify-footer-checks.js +103 -0
  127. package/src/verification/verify-footer-helpers.js +178 -0
  128. package/src/verification/verify-footer.js +135 -0
  129. package/src/verification/verify-header-checks.js +104 -0
  130. package/src/verification/verify-header-helpers.js +156 -0
  131. package/src/verification/verify-header.js +144 -0
  132. package/src/verification/verify-layout-report.js +101 -0
  133. package/src/verification/verify-layout.js +14 -260
  134. package/src/verification/verify-menu-checks.js +104 -0
  135. package/src/verification/verify-menu-helpers.js +112 -0
  136. package/src/verification/verify-menu.js +18 -302
  137. package/src/verification/verify-slider-checks.js +115 -0
  138. package/src/verification/verify-slider-constants.js +65 -0
  139. package/src/verification/verify-slider-helpers.js +164 -0
  140. package/src/verification/verify-slider.js +142 -0
  141. package/.env.example +0 -14
  142. package/docs/basic-clone.md +0 -63
  143. package/docs/cli-reference.md +0 -118
  144. package/docs/design-clone-architecture.md +0 -275
  145. package/docs/pixel-perfect.md +0 -86
  146. package/docs/troubleshooting.md +0 -169
  147. package/requirements.txt +0 -5
  148. package/src/ai/analyze-structure.py +0 -305
  149. package/src/ai/extract-design-tokens.py +0 -439
  150. package/src/ai/prompts/__init__.py +0 -2
  151. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  152. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  153. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  154. package/src/ai/prompts/design_tokens.py +0 -183
  155. package/src/ai/prompts/structure_analysis.py +0 -273
  156. package/src/core/animation-extractor.js +0 -526
  157. package/src/core/design-tokens.js +0 -103
  158. package/src/core/dimension-extractor.js +0 -366
  159. package/src/core/dimension-output.js +0 -208
  160. package/src/core/discover-pages.js +0 -314
  161. package/src/core/extract-assets.js +0 -468
  162. package/src/core/filter-css.js +0 -499
  163. package/src/core/html-extractor.js +0 -171
  164. package/src/core/merge-css.js +0 -407
  165. package/src/core/multi-page-screenshot.js +0 -377
  166. package/src/core/rewrite-links.js +0 -226
  167. package/src/core/screenshot.js +0 -572
  168. package/src/core/state-capture.js +0 -602
  169. package/src/core/video-capture.js +0 -540
  170. package/src/utils/__init__.py +0 -16
  171. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  173. package/src/utils/env.py +0 -134
  174. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,191 @@
1
+ /**
2
+ * DOM Tree Analyzer
3
+ *
4
+ * Traverse DOM tree hierarchically to capture structure,
5
+ * semantic landmarks, and parent-child relationships.
6
+ *
7
+ * Key features:
8
+ * - PreOrder traversal (parent before children)
9
+ * - W3C landmark detection (header, main, footer, nav, aside)
10
+ * - Section context mapping (hero, content, sidebar, footer)
11
+ * - Bidirectional parent-child refs
12
+ * - Configurable max depth (default: 8)
13
+ *
14
+ * Post-processing (landmarks map, heading tree, stats) lives in
15
+ * dom-tree-analyzer-tree-builders.js and runs in Node context.
16
+ */
17
+
18
+ import { buildLandmarksMap, buildHeadingTree, countTreeStats } from './dom-tree-analyzer-tree-builders.js';
19
+
20
+ // Constants
21
+ export const MAX_DEPTH = 8;
22
+ export const LANDMARK_TAGS = ['header', 'main', 'footer', 'nav', 'aside', 'section', 'article'];
23
+ export const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
24
+
25
+ // Section detection thresholds
26
+ const HERO_THRESHOLD = 0.15;
27
+ const FOOTER_THRESHOLD = 0.85;
28
+ const SIDEBAR_MAX_WIDTH = 400;
29
+ const Y_POSITION_TOLERANCE = 5;
30
+
31
+ /**
32
+ * Extract DOM tree hierarchy from page.
33
+ * @param {import('playwright').Page} page - Playwright page
34
+ * @param {Object} options
35
+ * @param {number} [options.maxDepth=8] - Maximum traversal depth
36
+ * @param {boolean} [options.includeHidden=false] - Include hidden elements
37
+ * @returns {Promise<Object>} DOMHierarchy with root, landmarks, headingTree, stats
38
+ */
39
+ export async function extractDOMHierarchy(page, options = {}) {
40
+ const { maxDepth = MAX_DEPTH, includeHidden = false } = options;
41
+ const startTime = Date.now();
42
+
43
+ // Run DOM traversal inside browser context
44
+ const result = await page.evaluate(
45
+ ({ maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH }) => {
46
+ const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
47
+ const pageWidth = document.documentElement.clientWidth;
48
+
49
+ function detectRole(el) {
50
+ const tag = el.tagName.toLowerCase();
51
+ const ariaRole = el.getAttribute('role');
52
+ if (ariaRole) return ariaRole;
53
+
54
+ if (LANDMARK_TAGS.includes(tag)) {
55
+ const isTopLevel = !el.closest('main, section, article, aside');
56
+ if (tag === 'header' || tag === 'footer') {
57
+ return isTopLevel ? `${tag}-landmark` : `${tag}-section`;
58
+ }
59
+ return tag;
60
+ }
61
+
62
+ if (HEADING_TAGS.includes(tag)) return `heading-${tag.slice(1)}`;
63
+
64
+ if (tag === 'div' || tag === 'span') {
65
+ const cls = (el.className || '').toString().toLowerCase();
66
+ if (cls.includes('container')) return 'container';
67
+ if (cls.includes('wrapper')) return 'wrapper';
68
+ if (cls.includes('card')) return 'card';
69
+ if (cls.includes('grid')) return 'grid';
70
+ if (cls.includes('hero')) return 'hero';
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ function detectSectionContext(el, yPos) {
77
+ const tag = el.tagName.toLowerCase();
78
+ if (tag === 'header' || el.closest('header')) return 'header';
79
+ if (tag === 'footer' || el.closest('footer')) return 'footer';
80
+ if (tag === 'aside' || el.closest('aside')) return 'sidebar';
81
+ if (tag === 'nav' || el.closest('nav')) return 'nav';
82
+
83
+ const yRatio = yPos / pageHeight;
84
+ if (yRatio < HERO_THRESHOLD) return 'hero';
85
+ if (yRatio > FOOTER_THRESHOLD) return 'footer';
86
+
87
+ const computed = window.getComputedStyle(el);
88
+ const rect = el.getBoundingClientRect();
89
+ if ((computed.position === 'fixed' || computed.position === 'sticky') && rect.width < SIDEBAR_MAX_WIDTH) {
90
+ return 'sidebar';
91
+ }
92
+
93
+ return 'content';
94
+ }
95
+
96
+ function traverseDOM(el, depth, parentId, path) {
97
+ if (depth > maxDepth) return null;
98
+ const rect = el.getBoundingClientRect();
99
+ if (!includeHidden && rect.width === 0 && rect.height === 0) return null;
100
+
101
+ const id = path.join('-');
102
+ const computed = window.getComputedStyle(el);
103
+ const yPos = rect.y + window.scrollY;
104
+
105
+ const node = {
106
+ id,
107
+ tagName: el.tagName.toLowerCase(),
108
+ depth,
109
+ role: detectRole(el),
110
+ section: detectSectionContext(el, yPos),
111
+ attributes: {
112
+ id: el.id || null,
113
+ className: el.className ? el.className.toString().split(' ').slice(0, 3).join(' ') : null,
114
+ role: el.getAttribute('role')
115
+ },
116
+ dimensions: {
117
+ width: Math.round(rect.width),
118
+ height: Math.round(rect.height),
119
+ x: Math.round(rect.x),
120
+ y: Math.round(yPos)
121
+ },
122
+ layout: {
123
+ display: computed.display,
124
+ position: computed.position !== 'static' ? computed.position : undefined,
125
+ flexDirection: computed.flexDirection !== 'row' ? computed.flexDirection : undefined,
126
+ gridTemplateColumns: computed.gridTemplateColumns !== 'none' ? computed.gridTemplateColumns : undefined
127
+ },
128
+ children: [],
129
+ parentId
130
+ };
131
+
132
+ let childIdx = 0;
133
+ for (const child of el.children) {
134
+ const childNode = traverseDOM(child, depth + 1, id, [...path, childIdx]);
135
+ if (childNode) { node.children.push(childNode); childIdx++; }
136
+ }
137
+
138
+ return node;
139
+ }
140
+
141
+ const root = traverseDOM(document.body, 0, null, [0]);
142
+ return { root, pageHeight, pageWidth };
143
+ },
144
+ { maxDepth, includeHidden, LANDMARK_TAGS, HEADING_TAGS, HERO_THRESHOLD, FOOTER_THRESHOLD, SIDEBAR_MAX_WIDTH }
145
+ );
146
+
147
+ // Post-process in Node context using tree-builder helpers
148
+ const landmarks = buildLandmarksMap(result.root);
149
+ const headingTree = buildHeadingTree(result.root);
150
+ const { totalNodes, maxDepth: maxActualDepth } = countTreeStats(result.root);
151
+
152
+ // Enrich headings with text + fontSize (separate evaluate for perf)
153
+ const headingData = await page.evaluate(({ headingTree, yTolerance }) => {
154
+ return headingTree.map(h => {
155
+ const headings = document.querySelectorAll(`h${h.level}`);
156
+ for (const el of headings) {
157
+ const rect = el.getBoundingClientRect();
158
+ const yPos = Math.round(rect.y + window.scrollY);
159
+ if (Math.abs(yPos - h.y) < yTolerance) {
160
+ const computed = window.getComputedStyle(el);
161
+ return {
162
+ ...h,
163
+ text: el.textContent?.trim().slice(0, 60) || null,
164
+ fontSize: parseFloat(computed.fontSize) || null
165
+ };
166
+ }
167
+ }
168
+ return h;
169
+ });
170
+ }, { headingTree, yTolerance: Y_POSITION_TOLERANCE });
171
+
172
+ const duration = Date.now() - startTime;
173
+ if (duration > 500) {
174
+ console.error(`[WARN] DOM hierarchy extraction took ${duration}ms (>500ms target)`);
175
+ }
176
+
177
+ return {
178
+ root: result.root,
179
+ landmarks,
180
+ headingTree: headingData,
181
+ stats: {
182
+ totalNodes,
183
+ maxDepth: maxActualDepth,
184
+ landmarkCount: [landmarks.header, landmarks.main, landmarks.footer].filter(Boolean).length +
185
+ landmarks.nav.length + landmarks.aside.length,
186
+ pageHeight: result.pageHeight,
187
+ pageWidth: result.pageWidth,
188
+ extractionTimeMs: duration
189
+ }
190
+ };
191
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Browser-side state capture for SPA app state snapshots.
3
+ *
4
+ * Runs page.evaluate calls to capture framework-specific data
5
+ * (__NEXT_DATA__, __NUXT__, etc.) and state management store state
6
+ * (Redux, Vuex, Pinia, Zustand, MobX). Used by app-state-snapshot.js.
7
+ */
8
+
9
+ /**
10
+ * Capture framework-specific data from page
11
+ * @param {import('playwright').Page} page - Playwright page
12
+ * @param {string|null} framework - Detected framework name
13
+ * @returns {Promise<Object|null>}
14
+ */
15
+ export async function captureFrameworkData(page, framework) {
16
+ try {
17
+ return await page.evaluate((fw) => {
18
+ switch (fw) {
19
+ case 'next':
20
+ if (!window.__NEXT_DATA__) return null;
21
+ return {
22
+ props: window.__NEXT_DATA__.props,
23
+ page: window.__NEXT_DATA__.page,
24
+ query: window.__NEXT_DATA__.query,
25
+ buildId: window.__NEXT_DATA__.buildId,
26
+ runtimeConfig: window.__NEXT_DATA__.runtimeConfig,
27
+ dynamicIds: window.__NEXT_DATA__.dynamicIds
28
+ };
29
+
30
+ case 'nuxt':
31
+ if (!window.__NUXT__) return null;
32
+ return {
33
+ data: window.__NUXT__.data,
34
+ state: window.__NUXT__.state,
35
+ serverRendered: window.__NUXT__.serverRendered,
36
+ routePath: window.__NUXT__.routePath,
37
+ config: window.__NUXT__.config
38
+ };
39
+
40
+ case 'vue': {
41
+ const vueApp = document.querySelector('[data-v-app]')?.__vue_app__;
42
+ if (vueApp?.config?.globalProperties) {
43
+ return {
44
+ routePath: window.location.pathname,
45
+ hasRouter: !!vueApp.config.globalProperties.$router,
46
+ hasStore: !!vueApp.config.globalProperties.$store ||
47
+ !!vueApp.config.globalProperties.$pinia
48
+ };
49
+ }
50
+ return null;
51
+ }
52
+
53
+ case 'react': {
54
+ const reactRoot = document.getElementById('root') ||
55
+ document.querySelector('[data-reactroot]');
56
+ return reactRoot ? {
57
+ hasReactRoot: true,
58
+ rootId: reactRoot.id || null
59
+ } : null;
60
+ }
61
+
62
+ case 'angular': {
63
+ const appRoot = document.querySelector('app-root');
64
+ if (appRoot && window.ng?.probe) {
65
+ try {
66
+ const component = window.ng.probe(appRoot);
67
+ return {
68
+ componentName: component?.componentInstance?.constructor?.name,
69
+ hasRouter: !!component?.injector?.get?.('Router', null)
70
+ };
71
+ } catch {
72
+ return { hasAppRoot: true };
73
+ }
74
+ }
75
+ return appRoot ? { hasAppRoot: true } : null;
76
+ }
77
+
78
+ case 'svelte':
79
+ if (window.__sveltekit_data__) {
80
+ return window.__sveltekit_data__;
81
+ }
82
+ return null;
83
+
84
+ case 'astro': {
85
+ const islands = document.querySelectorAll('astro-island');
86
+ if (islands.length > 0) {
87
+ return {
88
+ islandCount: islands.length,
89
+ componentNames: Array.from(islands)
90
+ .map(i => i.getAttribute('component-export'))
91
+ .filter(Boolean)
92
+ };
93
+ }
94
+ return null;
95
+ }
96
+
97
+ default:
98
+ return null;
99
+ }
100
+ }, framework);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Capture state management store state (Redux, Vuex, Pinia, Zustand, MobX)
108
+ * @param {import('playwright').Page} page - Playwright page
109
+ * @returns {Promise<{type: string, state: Object|null}>}
110
+ */
111
+ export async function captureStoreState(page) {
112
+ try {
113
+ return await page.evaluate(() => {
114
+ // Redux - Method 1: Redux DevTools extension
115
+ if (window.__REDUX_DEVTOOLS_EXTENSION__) {
116
+ try {
117
+ const stores = window.__REDUX_DEVTOOLS_EXTENSION__.stores ||
118
+ window.__REDUX_DEVTOOLS_EXTENSION__.open?.() ||
119
+ null;
120
+ if (stores && typeof stores === 'object') {
121
+ const storeKeys = Object.keys(stores);
122
+ if (storeKeys.length > 0) {
123
+ const store = stores[storeKeys[0]];
124
+ if (store?.getState) {
125
+ return { type: 'redux', state: store.getState() };
126
+ }
127
+ }
128
+ }
129
+ } catch {
130
+ // Continue to other methods
131
+ }
132
+ }
133
+
134
+ // Redux - Method 2: Direct store on window
135
+ if (window.store?.getState) {
136
+ return { type: 'redux', state: window.store.getState() };
137
+ }
138
+
139
+ // Redux - Method 3: __REDUX_STATE__ hydration
140
+ if (window.__REDUX_STATE__) {
141
+ return { type: 'redux', state: window.__REDUX_STATE__ };
142
+ }
143
+
144
+ // Vuex - Nuxt 2 / Vue 2/3
145
+ if (window.$nuxt?.$store?.state) {
146
+ return { type: 'vuex', state: window.$nuxt.$store.state };
147
+ }
148
+ if (window.__VUEX__?.state) {
149
+ return { type: 'vuex', state: window.__VUEX__.state };
150
+ }
151
+
152
+ // Vuex via Vue app
153
+ const vueApp = document.querySelector('[data-v-app]')?.__vue_app__;
154
+ if (vueApp?.config?.globalProperties?.$store?.state) {
155
+ return { type: 'vuex', state: vueApp.config.globalProperties.$store.state };
156
+ }
157
+
158
+ // Pinia - Nuxt 3 / Vue 3
159
+ if (window.$nuxt?.$pinia?.state?.value) {
160
+ return { type: 'pinia', state: window.$nuxt.$pinia.state.value };
161
+ }
162
+ if (window.__PINIA__?.state?.value) {
163
+ return { type: 'pinia', state: window.__PINIA__.state.value };
164
+ }
165
+ if (vueApp?.config?.globalProperties?.$pinia?.state?.value) {
166
+ return { type: 'pinia', state: vueApp.config.globalProperties.$pinia.state.value };
167
+ }
168
+
169
+ // Zustand - check common window-exposed store names
170
+ const zustandPatterns = ['useStore', 'useAppStore', 'useBearStore', 'store'];
171
+ for (const pattern of zustandPatterns) {
172
+ const potentialStore = window[pattern];
173
+ if (potentialStore?.getState && typeof potentialStore.getState === 'function') {
174
+ try {
175
+ const state = potentialStore.getState();
176
+ if (state && typeof state === 'object') {
177
+ return { type: 'zustand', state };
178
+ }
179
+ } catch {
180
+ // Not a valid Zustand store
181
+ }
182
+ }
183
+ }
184
+
185
+ // MobX
186
+ if (window.__MOBX_STATE__) {
187
+ return { type: 'mobx', state: window.__MOBX_STATE__ };
188
+ }
189
+
190
+ return { type: 'none', state: null };
191
+ });
192
+ } catch {
193
+ return { type: 'none', state: null };
194
+ }
195
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Serialization and filtering utilities for app state snapshots.
3
+ *
4
+ * Provides safe object serialization (handles circular refs, functions, symbols),
5
+ * sensitive key filtering (tokens, passwords, secrets), and state size enforcement.
6
+ * Used by app-state-snapshot.js (main module).
7
+ */
8
+
9
+ import { SIZE_LIMITS } from '../../shared/config.js';
10
+
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+
15
+ /** Maximum state size in bytes (1MB) - sourced from centralized config */
16
+ export const MAX_STATE_SIZE = SIZE_LIMITS.MAX_STATE;
17
+
18
+ /** Maximum depth for recursive object traversal */
19
+ export const MAX_TRAVERSAL_DEPTH = 50;
20
+
21
+ /** Patterns to identify sensitive keys */
22
+ export const SENSITIVE_PATTERNS = [
23
+ /token/i,
24
+ /password/i,
25
+ /passwd/i,
26
+ /secret/i,
27
+ /auth/i,
28
+ /api[_-]?key/i,
29
+ /credential/i,
30
+ /private/i,
31
+ /session/i,
32
+ /cookie/i,
33
+ /bearer/i,
34
+ /jwt/i,
35
+ /access[_-]?key/i,
36
+ /refresh[_-]?token/i
37
+ ];
38
+
39
+ /** Marker for filtered sensitive values */
40
+ export const FILTERED_MARKER = '[FILTERED]';
41
+
42
+ /** Marker for circular references */
43
+ export const CIRCULAR_MARKER = '[Circular]';
44
+
45
+ /** Marker for unserializable values */
46
+ export const UNSERIALIZABLE_MARKER = '[Unserializable]';
47
+
48
+ // ============================================================================
49
+ // Utility Functions
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Check if a key matches sensitive patterns
54
+ * @param {string} key - Object key to check
55
+ * @returns {boolean}
56
+ */
57
+ export function isSensitiveKey(key) {
58
+ if (typeof key !== 'string') return false;
59
+ return SENSITIVE_PATTERNS.some(pattern => pattern.test(key));
60
+ }
61
+
62
+ /**
63
+ * Filter sensitive keys from an object recursively
64
+ * @param {*} obj - Object to filter
65
+ * @param {string[]} warnings - Array to collect warnings
66
+ * @param {string} path - Current path for warning messages
67
+ * @param {number} depth - Current recursion depth
68
+ * @returns {*} Filtered object
69
+ */
70
+ export function filterSensitive(obj, warnings = [], path = '', depth = 0) {
71
+ // Prevent infinite recursion
72
+ if (depth > MAX_TRAVERSAL_DEPTH) {
73
+ return '[Max Depth Exceeded]';
74
+ }
75
+
76
+ // Handle primitives
77
+ if (obj === null || obj === undefined) return obj;
78
+ if (typeof obj !== 'object') return obj;
79
+
80
+ // Handle arrays
81
+ if (Array.isArray(obj)) {
82
+ return obj.map((item, i) =>
83
+ filterSensitive(item, warnings, `${path}[${i}]`, depth + 1)
84
+ );
85
+ }
86
+
87
+ // Handle objects
88
+ const filtered = {};
89
+ for (const [key, value] of Object.entries(obj)) {
90
+ const fullPath = path ? `${path}.${key}` : key;
91
+
92
+ if (isSensitiveKey(key)) {
93
+ warnings.push(`Filtered sensitive key: ${fullPath}`);
94
+ filtered[key] = FILTERED_MARKER;
95
+ continue;
96
+ }
97
+
98
+ filtered[key] = filterSensitive(value, warnings, fullPath, depth + 1);
99
+ }
100
+
101
+ return filtered;
102
+ }
103
+
104
+ /**
105
+ * Safely serialize an object handling circular refs, functions, symbols
106
+ * @param {*} obj - Object to serialize
107
+ * @param {WeakSet} seen - Set of seen objects for circular detection
108
+ * @param {number} depth - Current recursion depth
109
+ * @returns {*} Serializable version of object
110
+ */
111
+ export function safeSerialize(obj, seen = new WeakSet(), depth = 0) {
112
+ if (depth > MAX_TRAVERSAL_DEPTH) return '[Max Depth Exceeded]';
113
+
114
+ if (obj === null || obj === undefined) return obj;
115
+ if (typeof obj === 'boolean' || typeof obj === 'number' || typeof obj === 'string') return obj;
116
+
117
+ if (typeof obj === 'function') return '[Function]';
118
+ if (typeof obj === 'symbol') return obj.toString();
119
+ if (typeof obj === 'bigint') return obj.toString();
120
+ if (obj instanceof Date) return obj.toISOString();
121
+ if (obj instanceof RegExp) return obj.toString();
122
+ if (obj instanceof Error) return { message: obj.message, name: obj.name };
123
+ if (obj instanceof Map) return Object.fromEntries(obj);
124
+ if (obj instanceof Set) return Array.from(obj);
125
+
126
+ if (typeof obj === 'object') {
127
+ if (seen.has(obj)) return CIRCULAR_MARKER;
128
+ seen.add(obj);
129
+
130
+ try {
131
+ if (Array.isArray(obj)) {
132
+ return obj.map(item => safeSerialize(item, seen, depth + 1));
133
+ }
134
+
135
+ const result = {};
136
+ for (const [key, value] of Object.entries(obj)) {
137
+ try {
138
+ result[key] = safeSerialize(value, seen, depth + 1);
139
+ } catch {
140
+ result[key] = UNSERIALIZABLE_MARKER;
141
+ }
142
+ }
143
+ return result;
144
+ } catch {
145
+ return UNSERIALIZABLE_MARKER;
146
+ }
147
+ }
148
+
149
+ return obj;
150
+ }
151
+
152
+ /**
153
+ * Enforce state size limit, truncating store state if exceeded
154
+ * @param {import('./app-state-snapshot.js').StateSnapshot} snapshot - State snapshot to check
155
+ * @param {string[]} warnings - Array to collect warnings
156
+ * @returns {import('./app-state-snapshot.js').StateSnapshot} Possibly truncated snapshot
157
+ */
158
+ export function enforceStateLimit(snapshot, warnings) {
159
+ const serialized = JSON.stringify(snapshot);
160
+ const sizeBytes = Buffer.byteLength(serialized, 'utf8');
161
+
162
+ if (sizeBytes > MAX_STATE_SIZE) {
163
+ const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
164
+ warnings.push(`State exceeded 1MB limit (${sizeMB}MB), store state truncated`);
165
+
166
+ return {
167
+ ...snapshot,
168
+ storeState: {
169
+ _truncated: true,
170
+ _reason: `exceeded 1MB limit (${sizeMB}MB)`,
171
+ _originalType: snapshot.storeType
172
+ },
173
+ sizeBytes: MAX_STATE_SIZE
174
+ };
175
+ }
176
+
177
+ return { ...snapshot, sizeBytes };
178
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * App State Snapshot Module
3
+ *
4
+ * Captures application state from SPAs including:
5
+ * - Framework data (__NEXT_DATA__, __NUXT__)
6
+ * - State management stores (Redux, Vuex, Pinia, Zustand)
7
+ *
8
+ * Features:
9
+ * - Sensitive data filtering (tokens, passwords, secrets)
10
+ * - Safe serialization (handles circular refs, functions, symbols)
11
+ * - Size limit enforcement (1MB max)
12
+ *
13
+ * @module app-state-snapshot
14
+ */
15
+
16
+ import {
17
+ MAX_STATE_SIZE,
18
+ SENSITIVE_PATTERNS,
19
+ FILTERED_MARKER,
20
+ CIRCULAR_MARKER,
21
+ isSensitiveKey,
22
+ filterSensitive,
23
+ safeSerialize,
24
+ enforceStateLimit
25
+ } from './app-state-snapshot-utils.js';
26
+
27
+ import {
28
+ captureFrameworkData,
29
+ captureStoreState
30
+ } from './app-state-snapshot-capture.js';
31
+
32
+ // Re-export constants and utilities for backward compatibility
33
+ export {
34
+ MAX_STATE_SIZE,
35
+ SENSITIVE_PATTERNS,
36
+ FILTERED_MARKER,
37
+ CIRCULAR_MARKER,
38
+ isSensitiveKey,
39
+ filterSensitive,
40
+ safeSerialize,
41
+ enforceStateLimit,
42
+ captureFrameworkData,
43
+ captureStoreState
44
+ };
45
+
46
+ // ============================================================================
47
+ // Type Definitions (JSDoc)
48
+ // ============================================================================
49
+
50
+ /**
51
+ * @typedef {Object} StateSnapshot
52
+ * @property {Object|null} frameworkData - __NEXT_DATA__, __NUXT__, etc.
53
+ * @property {Object|null} storeState - Redux/Vuex/Pinia/Zustand state
54
+ * @property {string|null} framework - Detected framework name
55
+ * @property {string} storeType - 'redux'|'vuex'|'pinia'|'zustand'|'none'
56
+ * @property {string[]} warnings - Serialization/filtering warnings
57
+ * @property {number} capturedAt - Unix timestamp
58
+ * @property {number} sizeBytes - Serialized size in bytes
59
+ */
60
+
61
+ // ============================================================================
62
+ // Main Export
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Capture application state from page
67
+ * @param {import('playwright').Page} page - Playwright page instance
68
+ * @param {Object|null} [frameworkInfo] - Framework detection result
69
+ * @returns {Promise<StateSnapshot>}
70
+ */
71
+ export async function captureAppState(page, frameworkInfo = null) {
72
+ const warnings = [];
73
+ const framework = frameworkInfo?.framework || null;
74
+
75
+ let snapshot = {
76
+ frameworkData: null,
77
+ storeState: null,
78
+ framework,
79
+ storeType: 'none',
80
+ warnings,
81
+ capturedAt: Date.now(),
82
+ sizeBytes: 0
83
+ };
84
+
85
+ try {
86
+ const rawFrameworkData = await captureFrameworkData(page, framework);
87
+ if (rawFrameworkData) {
88
+ const serialized = safeSerialize(rawFrameworkData);
89
+ snapshot.frameworkData = filterSensitive(serialized, warnings);
90
+ }
91
+
92
+ const storeResult = await captureStoreState(page);
93
+ if (storeResult.state) {
94
+ const serialized = safeSerialize(storeResult.state);
95
+ snapshot.storeState = filterSensitive(serialized, warnings);
96
+ snapshot.storeType = storeResult.type;
97
+ }
98
+
99
+ snapshot = enforceStateLimit(snapshot, warnings);
100
+ } catch (error) {
101
+ warnings.push(`State capture error: ${error.message}`);
102
+ }
103
+
104
+ return snapshot;
105
+ }
106
+
107
+ /**
108
+ * Format state snapshot for logging
109
+ * @param {StateSnapshot} snapshot - Captured state
110
+ * @returns {string}
111
+ */
112
+ export function formatStateSnapshot(snapshot) {
113
+ const lines = [
114
+ '\n=== App State Snapshot ===',
115
+ `Framework: ${snapshot.framework || 'unknown'}`,
116
+ `Store Type: ${snapshot.storeType}`,
117
+ `Framework Data: ${snapshot.frameworkData ? 'captured' : 'none'}`,
118
+ `Store State: ${snapshot.storeState ? 'captured' : 'none'}`,
119
+ `Size: ${(snapshot.sizeBytes / 1024).toFixed(2)} KB`
120
+ ];
121
+
122
+ if (snapshot.warnings.length > 0) {
123
+ lines.push(`Warnings (${snapshot.warnings.length}):`);
124
+ snapshot.warnings.slice(0, 5).forEach(w => lines.push(` - ${w}`));
125
+ if (snapshot.warnings.length > 5) {
126
+ lines.push(` ... and ${snapshot.warnings.length - 5} more`);
127
+ }
128
+ }
129
+
130
+ return lines.join('\n');
131
+ }